垃圾收集算法及各种垃圾收集器
2022-10-07 13:51:27 0 举报
AI智能生成
垃圾收集算法及各种垃圾收集器
作者其他创作
大纲/内容
垃圾收集算法
分代收集理论
根据对象存活周期的不同将内存分为几块,堆一般分为年轻代和老年代,根据各个代的特点选择合适的垃圾收集算法
标记-复制
将内存分为大小相同的两块,每次使用其中的一块,当一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
存在问题:空间只有一半
标记-清除
标记存活的对象,回收未被标记的对象(一般这种),或者标记所有需要回收的对象,然后统一回收
存在问题:
1、效率问题(需要标记的对象太多,效率不高)
2、空间问题(标记清除后会有大量不连续碎片)
1、效率问题(需要标记的对象太多,效率不高)
2、空间问题(标记清除后会有大量不连续碎片)
标记-整理
标记出存活的对象,让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存(根据老年代特点推出的一种标记算法)
垃圾收集器
垃圾收集器是垃圾收集算法的具体实现,生产上根据具体场景选择适合的垃圾收集器
Serial/SerialOld
(-XX:+UseSerialGC -XX:+UseSerialOldGC)
(-XX:+UseSerialGC -XX:+UseSerialOldGC)
单线程收集器,“单线程”指的是它仅会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(STW),直到它收集结束。新生代采用复制算法,老年代采用标记-整理算法。
优点:简单而高效,Serial收集器由于没有线程交互开销,可以获得很高的单线程搜集效率
Serial Old收集器两大用途
JDK5及之前中与Parallel Scavenge收集器搭建使用
作为CMS收集器的后备方案
Parallel/Parallel Old
(-XX:+UseParallelGC -XX:+UseParallelOldGC)
(-XX:+UseParallelGC -XX:+UseParallelOldGC)
Serial收集器的多线程版本,除了使用多线程进行垃圾回收外,其他与Serial收集器类似
Parallel收集器默认的收集线程数跟cpu核数相同,也可使用参数-XX:ParallelGCThreads来指定收集线程数
Parallel收集器默认的收集线程数跟cpu核数相同,也可使用参数-XX:ParallelGCThreads来指定收集线程数
优点:高效率使用CPU,提高吞吐量,其他垃圾收集器关注的是STW时间
在注重吞吐量和cpu资源的场合,可使用Parallel Scanvenge和Parallel Old收集器,这也是JDK8默认的新生代和老年代收集器
ParNew
(-XX:+UseParNewGC)
(-XX:+UseParNewGC)
新生代垃圾回收器,除了能与CMS搭配使用外,其余与Parallel类似
CMS(Concurrent Mark Sweep)
(-XX:+UseConcMarkSweepGC)
(-XX:+UseConcMarkSweepGC)
老年代垃圾回收器,该收集器以最短回收停顿时间为目标,虚拟机第一款真正意义上的并发收集器,实现了让垃圾收集器和用户线程同时工作
使用标记-清除算法,整个过程分为五部分:
初始标记:STW,记录下GC Roots直接引用的对象,速度很快
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,过程耗时较长但无需停止用户线程,让垃圾收集器线程与用户线程一起并发运行。但也因为用户程序并发运行,可能会导致已经标记过的对象状态发生改变
重新标记:为了修正并发标记期间因用户线程并发运行而导致标记状态发生改变的那部分对象的标记记录,这个阶段的停顿时间比初始标记常,但比并发标记时间短,主要用到了三色标记里的增量更新算法做重新标记
并发清理:GC线程对未标记的区域做清理,这个阶段如果有新增对象会被标记为黑色不做处理
并发重置:重置GC过程中的标记数据
初始标记:STW,记录下GC Roots直接引用的对象,速度很快
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,过程耗时较长但无需停止用户线程,让垃圾收集器线程与用户线程一起并发运行。但也因为用户程序并发运行,可能会导致已经标记过的对象状态发生改变
重新标记:为了修正并发标记期间因用户线程并发运行而导致标记状态发生改变的那部分对象的标记记录,这个阶段的停顿时间比初始标记常,但比并发标记时间短,主要用到了三色标记里的增量更新算法做重新标记
并发清理:GC线程对未标记的区域做清理,这个阶段如果有新增对象会被标记为黑色不做处理
并发重置:重置GC过程中的标记数据
优点:并发收集、低停顿
缺点:
1、对CPU资源敏感,会和应用服务抢资源
2、无法处理浮动垃圾,在并发标记和并发清理期间产生的垃圾,只能等到下一次GC处理
3、空间碎片,使用的标记-清除算法会导致空间碎片产生,可通过-XX:+UseCMSCompactAtFullCollection参数在清理后再做整理
4、不确定性,会存在上次垃圾回收还未完成,然后又被触发的情况,特别是在并发标记和并发清除阶段,回收与系统运行并发,也许还没回收完就再次出发了FullGC,就会concurrent mode failure,此时会进入STW,用Serial Old垃圾回收器来回收
1、对CPU资源敏感,会和应用服务抢资源
2、无法处理浮动垃圾,在并发标记和并发清理期间产生的垃圾,只能等到下一次GC处理
3、空间碎片,使用的标记-清除算法会导致空间碎片产生,可通过-XX:+UseCMSCompactAtFullCollection参数在清理后再做整理
4、不确定性,会存在上次垃圾回收还未完成,然后又被触发的情况,特别是在并发标记和并发清除阶段,回收与系统运行并发,也许还没回收完就再次出发了FullGC,就会concurrent mode failure,此时会进入STW,用Serial Old垃圾回收器来回收
实战案例讲解(ParNew + CMS)
背景:秒杀,每秒1000多单,三台后台服务器(4核8G),每台每秒处理300多单,每个订单对象假定为1KB,每秒300KB订单对象生成,因下单还涉及其他对象(库存、优惠券、积分等),放大20倍,则6000KB每秒(6MB),同时还有其它操作,再放大十倍,则每秒有60MB对象生成,1秒后都成为垃圾对象
原本JVM设置:java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=521M -XX:MaxMetaspaceSize=521M -jar microservice-eureka-server.jar
调整后JVM设置:会因为动态对象年龄判断原则经常产生FullGC,经过分析,将JVM参数调整为:java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=521M -XX:MaxMetaspaceSize=521M -jar microservice-eureka-server.jar
可能出现问题:秒杀场景下,某些时候服务器压力可能比预计的还大,交易处理时间会变长,那对象的存活时间可能会相应延长,导致对象可能两三秒后才能变为垃圾对象,以之前的分析,每次MinorGC后还剩余对象的大小可能为120-180M,导致会直接移动到老年代触发FullGC
原本JVM设置:java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=521M -XX:MaxMetaspaceSize=521M -jar microservice-eureka-server.jar
调整后JVM设置:会因为动态对象年龄判断原则经常产生FullGC,经过分析,将JVM参数调整为:java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=521M -XX:MaxMetaspaceSize=521M -jar microservice-eureka-server.jar
可能出现问题:秒杀场景下,某些时候服务器压力可能比预计的还大,交易处理时间会变长,那对象的存活时间可能会相应延长,导致对象可能两三秒后才能变为垃圾对象,以之前的分析,每次MinorGC后还剩余对象的大小可能为120-180M,导致会直接移动到老年代触发FullGC
优化思想:让短期存活的对象尽量都留在survivor中,不要进入老年代,这样minorgc就会回收掉,不用移到老年代触发FullGC
优化可考虑方面:
1、对象年龄多少移动到老年代:本例中一次MinorGC需要间隔二十几秒,大多数对象在几秒内就会变成垃圾,可以将默认的15改小,比如改为5,则意味着经过5次MinorGC才进入老年代,即经过120s后还未变成垃圾的对象才被移动到老年代,而不是一直在新生代区域复制
2、对象多大直接进入老年代:一般来说设置为1M,比如大的缓存List、Map对象
3、更换垃圾回收器:JDK8默认的垃圾回收器是Parallel、Parallel Old搭配,但如果内存较大(超过4G),系统对停顿时间比较敏感,可以使用ParNew+CMS
对于CMS垃圾回收器参数设置可以从以下考虑:
哪些对象会长期存活:bean、线程池对象、初始化的缓存数据等,这些一般也就几十M,还有就是某次MinorGC完了之后还有超过一两百M的对象存活,会直接进入老年代,如果5-6分钟出现一次这样的情况,那么大概半小时到一小时触发一次FullGC,因MinorGC挪动到老年代的对象一般不大,所以一般不会出现因为老年代空间分配担保机制触发FullGC,其实在秒杀场景下半小时或者一小时触发一次FullGC是可以接受的
碎片整理:因为触发FullGC差不多半小时至一小时一次,FullGC做完之后立即整理和两三次整理一次都行
其实,只要年轻代参数设置合理,老年代CMS的参数基本可用默认值,最后的调整参数为
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=92 ‐XX:+UseCMSCompactAtFullCollection ‐XX:CMSFullGCsBeforeCompaction=0
1、对象年龄多少移动到老年代:本例中一次MinorGC需要间隔二十几秒,大多数对象在几秒内就会变成垃圾,可以将默认的15改小,比如改为5,则意味着经过5次MinorGC才进入老年代,即经过120s后还未变成垃圾的对象才被移动到老年代,而不是一直在新生代区域复制
2、对象多大直接进入老年代:一般来说设置为1M,比如大的缓存List、Map对象
3、更换垃圾回收器:JDK8默认的垃圾回收器是Parallel、Parallel Old搭配,但如果内存较大(超过4G),系统对停顿时间比较敏感,可以使用ParNew+CMS
对于CMS垃圾回收器参数设置可以从以下考虑:
哪些对象会长期存活:bean、线程池对象、初始化的缓存数据等,这些一般也就几十M,还有就是某次MinorGC完了之后还有超过一两百M的对象存活,会直接进入老年代,如果5-6分钟出现一次这样的情况,那么大概半小时到一小时触发一次FullGC,因MinorGC挪动到老年代的对象一般不大,所以一般不会出现因为老年代空间分配担保机制触发FullGC,其实在秒杀场景下半小时或者一小时触发一次FullGC是可以接受的
碎片整理:因为触发FullGC差不多半小时至一小时一次,FullGC做完之后立即整理和两三次整理一次都行
其实,只要年轻代参数设置合理,老年代CMS的参数基本可用默认值,最后的调整参数为
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=92 ‐XX:+UseCMSCompactAtFullCollection ‐XX:CMSFullGCsBeforeCompaction=0
三色标记算法
将GC Roots可达性分析遍历对象过程中遇到的对象,根据“是否被访问过”标记成3种颜色
黑色
该对象及其所有引用已被垃圾回收器访问过,是存活的
灰色
对象已被垃圾回收器访问过,但这个对象上至少还有一个引用没被扫描过
白色
还未被访问过
在并发标记、并发清除过程中,由于用户线程和垃圾回收线程并发执行,可能会出现多标和漏标的情况
多标-浮动垃圾(本该为白色即不标记的标记了颜色)
1、方法结束会导致部分局部变量GCroots被销毁,这个GCroots引用的对象之前已经扫描过且被标记为黑色,那么本轮GC就不会回收这部分内存
2、在并发标记和并发清理阶段新产生的对象,可能会变成垃圾,一般是标记为黑色,本次GC不会进行清理
2、在并发标记和并发清理阶段新产生的对象,可能会变成垃圾,一般是标记为黑色,本次GC不会进行清理
GC线程扫描到B且标记为灰色,然后应用线程指向了断开A指向B的指针,因为A已经被标记为黑色,不会再重新扫描了,所以B C D在此轮不会被回收
漏标-读写屏障(本该标记颜色的对象未被标记颜色)
GC线程扫描到B且将其标记为灰色,此时切换用户线程执行了B断开到D的关联,增加A指向到D的关联,因为A已经被标记为黑色,不会重新扫描,所以D不会被标记,还是白色,最后会被回收掉。
漏标需要满足两个条件:
1、新增了黑色到白色的引用
2、删除了灰色到白色的引用
漏标需要满足两个条件:
1、新增了黑色到白色的引用
2、删除了灰色到白色的引用
解决漏标方式
增量更新,记录第一个条件
当黑色对象插入新的白色对象的引用关系时,就将这个新插入的引用记录下来,在重新标记阶段,在将这些记录过的引用关系中的黑色对象为根,重新扫描,可以简单理解为,黑色对象一旦新插入了指向白色对象的引用后,他就会变为灰色对象
原始快照(SATB),记录第二个条件
当灰色对象要删除指向白色对象的引用关系时,将其记录下来,等到重新标记时,将这些记录过的引用关系中灰色对象作为根,重新扫描一次(扫描快照么),这样就能扫描到白色的对象,将其标记为黑色
写屏障
上述对引用关系变化的记录,是使用写屏障实现的
写屏障就是在赋值操作前后,加入一些处理(参考AOP),写前操作、写后操作
写屏障就是在赋值操作前后,加入一些处理(参考AOP),写前操作、写后操作
写前屏障实现SATB
void pre_write_barrier(oop* field){
oop old_value = *filed;//获取旧值
remark_set.add(old_value);//记录原来的引用对象,对应上图为G
}
oop old_value = *filed;//获取旧值
remark_set.add(old_value);//记录原来的引用对象,对应上图为G
}
写后屏障实现增量更新
void post_write_barrier(oop* field, oop new_value){
remark_set.add(new_value);//记录新引用的值,对应上图为G
}
remark_set.add(new_value);//记录新引用的值,对应上图为G
}
CMS:写屏障+增量更新
G1、Shenandoah:写屏障+SATB
ZGC:读屏障
G1、Shenandoah:写屏障+SATB
ZGC:读屏障
记忆集与卡表
为解决跨代引用问题,老年代引用年轻代对象
记录集:记录非收集区域到收集区域的指针集合
卡表:记录集思想的具体实现,使用字节数组实现CARD_TABLE[],每个元素对应着其标识的内存区域一块特定大小的内存块,称为卡页
一个卡页只要存在一个对象字段存在跨代指针,其对应的卡表元素标识就变为1,标识该元素变脏
虚拟机使用写屏障维护卡表状态
一个卡页只要存在一个对象字段存在跨代指针,其对应的卡表元素标识就变为1,标识该元素变脏
虚拟机使用写屏障维护卡表状态
0 条评论
下一页