HotSpot JVM入门学习
类加载
加载过程
1.加载Loading
- 通过一个类的全限名获取此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区(1.8为元空间实现,1.8前永久代)的运行时数据结构
- 在内存中生成代表该类的java.lang.Class对象,作为方法区这个类的访问人口
2.连接Linking
2.2验证
- 确保Class文件的字节流中的信息符合虚拟机规范,保证加载类的正确性,不会危害虚拟机的自身安全。
- 验证阶段大致四个动作:文件格式验证、元数据验证、字节码验证、符号引用验证
2.3准备
- 为类变量(static)分配内存并设置默认初始值。
- final修饰的类变量在编译时分配,准备阶段显式初始化。
2.4解析
将常量池内的符号引用转化为直接引用
3.初始化Initialization
- 初始化阶段就是执行类构造器方法
<clinit>()
的过程,<clinit>()
是编译器自动收集所有类变量的赋值动作和静态代码块中的语句合并生成。 - 编译器收集顺序按源文件顺序决定。
<clinit>()
不同于类的构造器(构造器对应虚拟机视角下的<init>()
),若该类有父类,父类<clinit>()
方法会在子类的<clinit>()
方法前调用。- 虚拟机保证在一个类的
<clinit>()
方法在多线程下被同步加锁,保证一个类只被加载一次。
类加载器
启动类加载器Bootstrap classLoader
顶层类加载器底层C++实现,主要加载<JAVA_HOME>\lib目录下且被JVM能识别的类库至内存
扩展类加载器ExtClassLoader
主要负责加载<JAVA_HOME>\lib\ext目录下的一些扩展的jar,开发者可直接使用的加载器。
使用ExtClassLoader.getParent得到的加载器为null?是否说ExtClassLoader的父类加载器为null?
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (lock) {
...
if (target == null) {
try {
if (parent != null) {
target = parent.loadClass(name, false);
} else {
// 所有父类加载器为null的加载器,其父类都为启动类加载器BootstrapClassloader
target = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {...}
...
}
}
private Class<?> findBootstrapClassOrNull(String name){
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
private native Class<?> findBootstrapClass(String name);// native方法,通过JNI调用底层C
应用程序类加载器APPClassLoader
主要负责加载用户类路径(ClassPath)下的所有类库,父类加载器为ExtClassLoader。
- 自定义类加载器默认使用ApplicationClassLoader进行加载,系统核心类库都是使用引导类加载器进行加载
- ClassLoader只负责类的加载,执行引擎(Execution Engine)决定是否可运行
双亲委派机制
Java虚拟机对class文件采用按需引入,即需要使用该类时才会将他的class文件加载到内存生成class对象。
工作原理
- 当一个类加载器收到了类加载的请求时,不会自己去加载,而是把请求交给父类的加载器去执行。
- 如果父类加载器还有父类加载器时,进一步向上委托,依次递归,请求最终到达启动类加载器。
- 如果父类记载器可以完成类加载,超过返回。若父类无法完成加载,子类才会尝试加载。
为什么需要双亲委派
确保类的全局唯一性通过双亲委托机制,Java虚拟机总是先从最可信的Java核心API查找类型,可以防止不可信的类假扮被信任的类对系统造成危害。
JMM
虚拟机栈VM Stack
线程私有,生命周期与线程相同,每个方法执行时同步创建一个栈帧(Stack Frame),java虚拟机以方法为最基本的执行单元,栈帧存储了方法的存储局部变量表、操作栈、动态链接、返回地址等信息
局部变量表
最基本存储单元是Slot(变量槽),表中存放编译期可知的基本数据类型,对象引用(reference)和returnAddress,32位内的类型占1个Slot,64位的类型占2个Slot
动态链接
指向运行时常量池的方法引用
StackOverflowError
jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。
当执行A时,A调用B,B又重新调A,循环递归,持续压栈导致栈溢出。
可通过改变-Xss设置栈深度参数,Idea Run->Edit Configurations->VM options
堆Heap
线程共享,在虚拟机启动时创建。存储对象实例及数组,GC的主要作用区域。
GC
所有通过new创建的对象的内存都在堆中分配,其大小可以通过
-Xmx
和-Xms
来控制。堆被划分为新生代和老年
代,新生代又被进一步划分为伊甸园区和幸存区,幸存区由FromSpace和ToSpace组成。
新生实例对象在伊甸园区被创建,当伊甸园空间满时触发垃圾回收Minor GC
,对伊甸园区和ServivorFrom中的对象进行判断,销毁未被引用的对象,剩余的对象移动到空的Servivor区域,
即ServivorTo区(ServivorTo和ServivorFrom是交替的,谁空谁是ServivorTo),每移动一次对象对应的年龄计数器+1,当达到15(-XX:MaxTenuringThreshold
)时进行Promotion,晋升至老年代。
Minor GC
对新生代的垃圾进行收集
伊甸园区满时触发,Minor GC会引发STW,暂停用户线程,等垃圾回收结束,用户线程才恢复进行。
Major GC
对老年代的垃圾进行收集
老年代空间不足时,尝试触发Minor GC,之后空间仍不足触发Major GC
GC常用算法
复制算法
该算法将内存(Servivor)平均分成两部分(From\To),然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存(From)清空,循环下去。
优点:减少碎片
缺点:存在部分空间浪费
标记-清除法
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行GC操作。
优点:不需要移动对象,简单高效
缺点:过程效率低,会产生内存碎片
标记压缩法
该算法标记阶段和清除算法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存
强引用-Strong Reference
软引用-Soft Reference
弱引用-Weak Reference:
虚引用-Phantom Reference
OOM
方法区
线程共享内存区域,存储已被虚拟机加载的类型信息
、常量 、静态变量 、运行时常量池,即时编译器编译后的代码等数据。
JDK8 元空间与永久代
参考资料
深入了解Java虚拟机 JVM高级特性与最佳实践第三版 周志明
尚硅谷JVM详解java虚拟机
Java虚拟机(JVM)你只要看这一篇就够了!
Java内存区域(运行时数据区域)和内存模型(JMM)