类加载机制
类的生命周期
加载、验证、准备、解析、初始化
准备阶段:为类的 static 变量在方法区分配内存,将其初始值置为 0。被 final 修饰的常量会直接赋值。
JVM 内存结构
- 线程私有:Java虚拟栈、程序计数器、本地方法栈
- 线程共享:方法区、堆内存
双亲委派机制
Java 在尝试加载一个类时,系统首先判断这个类是否被加载过,已经加载的类直接返回,否则才会尝试加载。类加载器会将加载任务先委托给它的父加载器去尝试加载这个类,一直到达顶层的类(Bootstrap)只有在父加载器无法加载该类时(找不到对应的类),子加载器才会尝试去加载。
JVM 虚拟机栈
每个Java虚拟机线程都有一个私有的Java栈,与线程同时创建。Java栈中保存着帧信息,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
局部变量表、操作数栈、动态链接、方法返回地址。
堆
GC 垃圾回收,主要是在伊甸园区和养老区
新生代:诞生、成长、死亡的地方。分为伊甸园区、From、To 幸存区(0,1)。
老年代
晋升到老年代
- 在程序运行过程中,新生代GC会反复发生,长寿对象会在S0和S1之间反复交换,年龄也会越来越大,当对象达到年龄上限时,会被晋升到老年代。这个年龄上限默认是15。
- 大对象会跨过年轻代直接分配到老年代。
- 某个年龄和以下年龄的对象总大小大于幸存区的一半,年龄大于或等于该年龄的对象可以进入老年代。
- 老年代空间担保原则:在做任何一次 Minor GC 时,老年代需要确保自己的内存大于新生代所有对象的总和。如果设置了HandlePromotionFailure参数,还会进一步判断是否大于之前 Minor GC 进入老年代对象的平均大小。没有的话就直接 Full GC。
现在可以尝试 Minor GC 了,如果 Minor GC 后,存活的对象小于幸存区大小,就进入幸存区。如果大于幸存区,但小于老年代空间,就进入老年代。如果既大于幸存区又大于老年区,触发 Full GC。
在 jdk8 后,永久代改了个名字(元空间):这个区域在本地内存,存储的是 java 运行时的一些环境或类信息。这个区域不存在垃圾回收。方法区在元空间
非堆内存:方法区、元空间
- JDK 1.8 之后,无永久代,替代为元空间,不存在于 JVM,使用的是本地内存。
垃圾判断算法
- 引用计数算法:两个对象循环依赖导致无法回收。
- 可达性分析算法:通过GC Roots 开始找,与它相连接的就是可达对象。
- 三色标记算法
初始标记:寻找所有被 GCRoots 直接引用的对象。这里需要 Stop the World。
并发标记:对初始标记中标记的对象进行整个引用链的扫描,这里可以与用户线程同步。
重新标记:并发标记会耗费较长时间,这段时间中对象的引用可能会改变,用来处理这些问题。该过程需要 Stop the World。
并发清除:将标记为垃圾的对象进行清除,不需要 Stop the World。
垃圾回收算法
标记-清除:将存活的对象进行标记,然后清除未标记的对象。(会产生内存碎片)
标记-整理:让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
复制算法:将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存满了就将存活对象复制到另一块,再把使用过的内存空间一次性清理。
分代收集:将内存划分为几块,如堆分为新生代和老年代。
新生代一般使用复制算法进行垃圾回收。复制算法将新生代分为 Eden区和两个幸存区(From 区、To 区)。当 Eden 区满时,触发 Minor GC,将存活对象移动到一个幸存区,然后清空 Eden 区。这个过程循环反复,直到一个幸存区满了,再触发 Minor GC,将 Eden 区和 From 区的存活对象移到另一个幸存区,清空这个幸存区。
老年代一般采用标记-清除和标记-整理算法。
GC 过程
- 新创建的对象大多分配在 Eden 区,当Eden区满了发生 Minor GC。
- GC活下来的对象放到幸存区的S0区,S0区满了触发 Minor GC,S0区存活的对象会放入S1区。
- S1满了之后GC,存活的对象放到S0区,这样反复每GC一次,对象的年龄就增加一,到达某个值后,就晋升到老年代。
垃圾回收器
Serial:单线程回收。新生代用复制算法,老年代用标记-整理算法。
Parallel Scavenge、Parallel Old:新生代用复制算法,老年代用标记-整理算法。
以上两种方式都会导致用户线程暂停,进行垃圾回收。
分代收集器:CMS 使用三色标记算法
分区收集器:G1、ZGC
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1216271933@qq.com