(一)JVM加载的开头步骤与理论
1.Java命令执行代码的大体流程:
自我理解:
其中调用LoadClass的类加载过程
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >>
使用 >> 卸载
- 加载:加载类的字节码文件
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用(注:就是将一些符号引用从指向指针或其他东西,改为指向引用类或方法的物理地址)
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
2.类加载器和双亲委派机制
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
- 扩展类加载:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR
类包
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那
些类(主要使用)
-
自定义加载器:负责加载用户自定义路径下的类包
类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。
JVM默认使用Launcher的
getClassLoader()方法返回的类加载器
AppClassLoader的实例加载我们
的应用程序。
所有类加载器最终实现loadClass类,而loadClass类用于实现双亲委派机制。
双亲委派机制:加载某个类时会
先委托父加载器寻找目标类,
找不到再
委托上层父加载器加载,如果所有
父加载器在自己的加载类路径下都
找不到目标类,则在
自己的类加载路径中
查找并载入目标类。(
先找父亲加载,不行再由儿子自己加载)
//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查当前类加载器是否已经加载了该类
Class<?> c = findLoadedClass(name);
if (c == null) { 9 long t0 = System.nanoTime();
try {
if (parent != null) { //如果当前加载器父加载器不为空则委托父加载器加载该类
c = parent.loadClass(name, false);
} else { //如果当前加载器父加载器为空则委托引导类加载器加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non‐null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class. 24 long t1 = System.nanoTime();
//都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 ‐ t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) { //不会执行
resolveClass(c);
}
return c;
}
}
问题1:为什么双亲委派机制要从底到上再到底,不直接从上到底?
答:如在wed应用程序,95%在底部就已经加载,再次使用就是就可以直接拿去用,如果是从上到底的方式,每次都要走顶部这一步,就比较耗内存,所以从底到顶再到底,虽然开始多走几步,但是为了方便日后使用
问题2:为什么要设计双亲委派机制
-
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
-
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证 被加载类的唯一性
(二)JVM的内部情况与操作
1.JVM整体结构及内存模型
2.JVM内存参数设置
关于元空间的JVM参数有两个: -XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize
: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize
: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M,达到该值就会触发 full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超 过-XX:Max
MetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:
PermSize
参数意思不一样,-
XX:PermSize
代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,
对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
3.关于在JVM的对象
解决并发问题的方法:
-
CAS ( compare and swap )
虚拟机采用
CAS
配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
-
本地线程分配缓冲( Thread Local Allocation Buffer,TLAB )
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在
Java
堆中预先分配一小块内存。通过
XX:+/
UseTLAB
参数来设定虚拟机是否使用
TLAB(JVM
会默认开启
XX:+