Jvm 垃圾回收机制
垃圾回收器:
-
串行【SerialGC】:
- 单线程垃圾回收器;
- 堆内存较小,合适个人电脑【cpu少】;
- 执行过程:
- 串行在新生代执行的算法是:复制,在老年代上执行的是:标记整理,分别运行;
- 触发垃圾回收,建立STW安全点,单线程进行垃圾回收,其他进行阻塞;
- 执行结束后,打开安全点;
-
吞吐量优先【ParallelGC】:
- 多线程【线程数量和cpu核数相关】,堆内存较大,多核cpu;
- STW【Stop the world,对所有线程建立:安全点,阻塞】时间最短【但是可能发生多次】阶段,cpu占用率很大;
- 单位时间内,STW时间最短【垃圾时间占比越低,吞吐量就越高,单次不一定最短】;
- 实现过程:
- 吞吐量优先:在新生代执行的算法是:复制,在老年代上执行的是:标记整理,分别运行,都是并行,多线程运行;
- 触发垃圾回收,建立STW安全点,多个线程进行垃圾回收;
-
响应时间优先【CSM】
-
多线程,堆内存较大,多核cpu;
-
尽可能让单次STW时间最短【但是可能发生多次】;
-
STW建立安全点,标记GC root对象【初始标记】,之后STW结束,和用户线程并发执行,并且进行并发标记其他对象,最后并发清理;
-
实现过程:
- 开启STW,建立安全点,由部分线程进行初始标记【通常为总线程的1/4】,其余线程阻塞;
- 初始标记结束后,初始标记线程开始并发标记,其余用户线程正常工作;
- 并发标记后,开启STW安全点,所有线程重新标记;
- 为了防止新生代生产对象对老年代进行引用,要做一次扫描,并进行可达性分析判断;
- 为了提升效率,可先对新生代进行一次垃圾回收,减小新生代对象数量;
- 最后结束STW,开启并发清理,其余用户线程正常工作;
- 这里使用标记清理算法,产生碎片化空间;
- 碎片过多,垃圾器会退化执行一次串行垃圾器【标记整理算法】;
- 当并发失败后,会产生Full GC,退化为串行垃圾器;
在总时间上,吞吐量优先的STW要小于响应时间优先;
-
Garbage First【jdk9中替代了CMS,G1】
- 同时注重吞吐量和低延迟,
- 超大堆内存,将堆划分为多个大小相等的区域region【每个区域可以独立作为伊甸园,幸存区,老年区】;
- 整体是标记整理算法,两个区域之间是复制算法;
- G1垃圾回收阶段:
- 新生代垃圾回收:
- 当触发垃圾回收时,建立STW会话,
- 阈值达到,拷贝到老年区,
- 达不到,拷贝到幸存区From;
- 新生代垃圾回收 + 并发标记【老年区标记】;
- 触发垃圾回收时,在新生代垃圾回收时,就会进行GC root根对象的初始标记;
- 当老年代空间达到阈值时,进行并发标记,不会建立STW会话;
- 当并发失败了:即标记速度小于产生垃圾的速度,会产生 full GC【多线程】;
- 混合收集【新生代 + 老年去收集】:
- 会对伊甸园 + 幸村区 + 老年区进行全面垃圾回收;
- 前面时并发标记,这里建立STW会话,进行最终标记;
- 在优先保证响应时间的情况下,拷贝尽可能多的老年区;
- 跨代引用记录,新生代引用老年代:
- 将老年区进一步划分为相同大小的区域,card,卡表;
- 将引用过的对象的卡,标记为dirty card;
- 新生代有一个Remembered Set会记录有哪些脏卡,在GC root遍历的时候,直接到记录的脏卡内遍历;
- 当每次引用变更时,都需要变更卡表中的脏卡;
- 卡表就是存放跨代引用记录的【对新生代是直接遍历】;
- 将变更放入一个 dirty card queue中,后续由线程更显;
- 新生代垃圾回收:
- 重新标记Remark
- 并发标记【存在引用状态,不确定状态【正在判断】,不存在引用状态【垃圾】】
- 存在当前对象没有被引用,标记为垃圾后,因为时并发执行,后续由用户在进行引用后,就不再是垃圾了【并发标记引起的错误】,因此需要进一步在STW下重新标记;
- 当在并发标记后,给垃圾加上一个写屏障,当对象引用发生了改变,就会执行写屏障的代码,
- 写屏障【pre-write barrier】:
- 将对象加入队列【stab_mark_queue】,并且将对象标记为不确定状态;
- 当重新标记的时候,会建立STW会话,重新判断;
- 并发标记【存在引用状态,不确定状态【正在判断】,不存在引用状态【垃圾】】
- 字符串去重:
- 原因:字符串对象的底层是由char[]实现的;
- 将所有新分配的字符串放入一个队列,
- 当新生代回收时,G1并发检查是否由字符串重复【是否由相同char[]内容生成的】
- 如果有,则让底层引用相同的char[]数组
- 与String.intern()不一样:
- String.intern()关注的是字符串对象;
- 而字符串去重关注的是底层:char[],
- 在jvm内部,两则使用了不同的字符串表;
- 节约内存,增加新生代回收时间;
- 并发标记类卸载;
- 所有对象经过并发标记后,知道哪些类不再使用;
- 当一个类加载器的所有的类都不在使用,则卸载所加载的所有类;
- 该类的实例对象【实例化】,该类的Class对象【无反射引用】,加载该类的ClassLoader【一般是自定义类加载器】;
- 回收巨型对象:
- 当一个对象大于region的一般时,则称为巨型对象,直接会存放入老年代区;
- G1不会堆巨型对象进行拷贝;
- 回收时,会被优先考虑;
- G1会跟踪老年带所有对巨型对象的引用,当老年代的卡表【跨代引用记录】对巨型对象的引用为0时,就可以在新生代垃圾回收时,处理掉;
- 并发标记起始时间的调整:
- 以前的阈值是45%d,超过这个就开始并发标记,混合收集
- 这里设置一个初始值,可以进行动态调整阈值【总会动态的调价一个安全的空档空间】;
-
新生代的特点
- 所有new操作的的分配非常廉价;
- 每个线程在伊甸园有一个私有空间:Tlab,当有空间时,会优先在这里分配new 对象,以实现对象分配的保护【防止多线程new 对象时,相互产生干扰】;
- 大部分对象用过即死;
- 死亡对象的回收代价是0,采用复制算法,将存活对象复制到幸存区TO;
- Minor GC的时间远远低于Full GC
- 所有new操作的的分配非常廉价;
-
新生代调优:
- 新生代是不是越大越好?
- 小了,空间小,容易触发 Minor GC;
- 大了,老年代空间小了,容易触发Full GC;
- 大于25%, 小于50%;
- 吞吐量和空间大小,存在一个二次函数关系,向下;
- 尽可能的大:新生代使用算法:复制【标记和复制】
- 主要消耗时间在复制上,但是新生代大部分对象用过即死,所以复制对象少;
- 新生代理想容量=并发量 * (一次请求响应的量)
- 幸存区的理想容量= 当前活跃【但不久死亡】+ 需要晋升的对像;
- 太大,需要晋升对象会在新生代经常复制,耗时;
- 太小,会将不久死亡的对象晋升到老年区;
- 新生代是不是越大越好?
-
老年代调优【CMS】
- 老年代内存,越大越好;
- 当没有Full GC 的时候,不用做调优;
- 当有时,优先新生代调优;
- 观察发生full GC时,老年代内存使用情况,将老年代调大到 1/ 4 或 1/3;
-

浙公网安备 33010602011771号