JVM(三) 垃圾收集器

一。概述 


收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
java虚拟机规范中对应垃圾收集器应该如何实现并没有任何规定,因此不同的厂商,不同版本的虚拟机所提供的垃圾收集器可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合各个年代所使用的收集器。
不同分代的收集器,如果两个收集器之间存在连线,就可以说明他们可以搭配使用:

二。垃圾收集器


1,Serial收集器(年轻代、复制算法、会STW)

串行收集器是最古老的收集器,只使用一个线程去回收,过程中会STW。

2,ParNew收集器(年轻代、复制算法、会STW

ParNew收集器其实就是 Serial收集器的多线程版本,其余行为包括所有控制参数(如:-XX:SurivivorRatio等)、收集算法、STW、等都与Serial收集器完全一样。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。
在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置

3,Paralle Scavenge收集器(年轻代、复制算法)

Parallel Scavenge收集器类似ParNew收集器,Parallel Scavenge收集器更关注系统的吞吐量。 吞吐量=运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)
可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息, 自动动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;
参数控制:  - XX :+ UseParallelGC 使用Parallel收集器
CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
值得注意的是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。

4,Parallel Old 收集器(老年代、标记整理算法)

Parallel Old是Parallel Scavenge的老年代版本,使用多线程,在JDK 1.6中才开始提供。
参数控制:  - XX :+ UseParallelOldGC 使用Parallel收集器
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器

5,CMS收集器(老年代、标记清除算法)

CMS(Concurrent Mark Sweep)收集器是一种以 获取最短回收停顿时间为目标的收集器, 新生代默认使用ParNew。

1)4个步骤:

1. 初始标记(initial mark)(STW)
仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
2. 并发标记(concurrent mark)
GC Roots Tracing,根据Roots找到可达的对象。
3. 重新标记(remark)(STW)
修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
4. 并发清除(concurrent sweep)
并发标记和并发清除是最耗时的,但收集器线程都可以与用户线程一起工作。

2)优缺点

优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量

3)参数控制

-XX:+UseConcMarkSweepGC (使用CMS收集器)
-XX:+UseCMSCompactAtFullCollection (FullGC后,进行一次碎片整理,整理过程是独占的,会引起停顿时间变长)
-XX:+CMSFullGCsBeforeCompaction (设置进行几次Full GC后,进行一次碎片整理)
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

4)几个性质和机制

① 跨带引用。
CMS存在新生代和老年代的互相引用。
② CMS重新标记阶段会扫描整个堆,包括新生代。
缘故:新生代GC和老年代的GC是各自分开独立进行的,只有Minor GC时才会使用根搜索算法,标记新生代对象是否可达,也就是说虽然一些对象已经不可达,但在Minor GC发生前不会被标记为不可达,CMS也无法辨认哪些对象存活,只能全堆扫描(新生代+老年代)。
堆中对象的数目直接影响了Remark阶段耗时。
③ 并发预清理(和2关联)
如果Remark前执行一次Minor GC,新生代大部分对象就会被回收。CMS就在Remark前增加了一个可中断的并发预清理(CMS-concurrent-abortable-preclean),该阶段主要工作仍然是并发标记对象是否存活,只是这个过程可被中断。此阶段在Eden区使用超过2M时启动,当然2M是默认的阈值,可以通过参数修改。如果此阶段执行时等到了Minor GC,那么上述灰色对象将被回收,Reamark阶段需要扫描的对象就少了,耗时就少了。
④ 卡表。
老年代也可能持有新生代对象引用,所以Minor GC时也必须扫描老年代。
JVM是如何避免Minor GC时扫描全堆的? 经统计老年代持有新生代对象引用的情况不足1%,Minor GC为了避免扫描全堆,引入卡表。
具体策略是将老年代的空间分成大小为512B的若干张卡(card)。当发生老年代引用新生代时,虚拟机通过卡表标记为脏(卡表还有另外的作用,标识并发标记阶段哪些块被修改过),之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。
⑥ 动态年龄计算
Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升到老年代年龄阈值。详见:从实际案例聊聊Java应用的GC优化 - 美团技术团队

5)失败处理

① 并发模式失败(concurrent mode failure)
原因:
A,老年代无法容纳新生代GC晋升的对象;
B,老年代回收时有业务线程试图将大对象放入老年代,导致老年代的回收慢于业务对象对老年代内存的分配;
此时CMS退化成完全STW的Full GC,也就是Serial Old,导致一个漫长的暂停。
解决:
A,更早的启动CMS收集。
CMS默认在老年代使用占到92%(jdk1.8)时启动CMS GC(注意不是Full GC),可以调小该阈值,并让CMS只根据老年代的使用比例来决定是否回收。
-XX:CMSInitiatingOccupancyFraction=N
-XX:+UseCMSInitiatingOccupancyOnly
B,更多的线程来运行CMS。
之所以出现并发模式失败,是因为CMS的速度跑不赢对象晋升到老年代的速度了。可以通过-XX:ConGCThreads=N来设置后台线程的数量。默认情况下线程数ConcGCThreads=(3+ParallelGCThreads)/4,是根据ParallelGCThreads来计算的。
② 晋升失败(promoration failure)
原因:
老年代有足够的空间,但是由于碎片化严重,无法容纳新生代中晋升的对象,发生晋升失败。
解决:
A,n次FullGC的后对老年代进行一次碎片压缩。
-XX:+UseCMSCompactAtFullCollection 
-XX:CMSFullGCsBeforeCompaction=n
这两个参数设置在进行多少次FullGC的时候对老年代的内存进行一次碎片整理压缩。默认n=0,即每次CMS GC顶不住了而转入Full GC的时候都会做压缩。
B,增加新生代或Survivor空间的大小,尽量把大对象控制在新生代;

6,G1收集器

G1(Garbage-First Garbage Collector)特别适用于多核处理器和大内存的机器。G1 GC在JDK 7u4版本中被正式推出,并且在JDK 9中成为默认的垃圾收集器。它的主要目标是在满足高吞吐量的同时,尽可能缩短垃圾收集造成的停顿时间。

1)对比CMS

  • G1优点:G1整体使用标记-整理算法,不会产生碎片;
  • G1缺点:更高的内存、CPU负载,G1需要维护Rset和Cset,以及G1不止用了同CMS的写后屏障,还用了写前屏障;
  • 适用于不同内存大小: 按照实践经验,在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。
以上只是理论上列举,但得结合具体情况分析,以及通过压测等手段验证。

2)主要特点

  • 并行:G1能够充分利用多核处理器的优势,通过并行执行垃圾收集任务来提高效率,从而减少了停顿时间。
  • 分区收集:G1将整个堆内存划分为多个大小相等的独立区域Region,这些区域在逻辑上是连续的,但在物理内存上可能不是连续的。每个Region都可以扮演Eden、Survivor、Old、Humongous区。这种设计使得G1 GC能够更加灵活地进行内存管理和垃圾收集。
  • 优先回收垃圾最多区域:G1通过跟踪每个Region中的垃圾堆积情况,并根据回收价值和成本进行排序,优先回收垃圾最多的Region。这种策略有助于最大限度地提高垃圾收集的效率。
  • 可预测的停顿时间:G1通过建立一个可预测的停顿时间模型(基于 衰减平均值理论,给近期的数据更高的权重,强调近期数据对结果的影响),允许用户明确指定在一个特定时间片段内,垃圾收集所造成的停顿时间不得超过某个阈值。这使得G1 GC非常适合需要严格控制停顿时间的应用场景。
  • 使用 标记-整理算法:在整体上,G1使用标记-整理算法来回收内存,以减少内存碎片的产生。但在两个Region之间进行垃圾收集时,它则采用 标记-复制算法。这种组合策略有助于兼顾内存利用率和垃圾收集效率。

3)内存分区

4)各回收模式和触发条件

①  Minor GC
何时触发
Eden分配不下对象时,G1估算MinorGC时间,可能不执行GC而是增加Eden区数量(默认在5%和60%浮动),也可能直接触发MinorGC,将Eden和S区复制到新的S区,如果达到晋升年龄或者S区放不下,就可能晋升到老年代。
注意:并不是Eden区满就一定会触发MinorGC!G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数-XX:MaxGCPauseMills设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMills设定的值,就会触发MinorGC。
回收过程
使用复制算法。直接将Eden和S区存活的对象集中复制到另外的S区里,该过程仍然是STW的,但是执行快、耗时少,也不会影响用户线程的执行。参考:G1垃圾收集器(6)之Young GC-腾讯云开发者社区-腾讯云
②  Mixed GC
何时触发
1. 新生代分配不了需要晋升老年代时,老年代空间使用过多,有可能触发Mixed GC;(TODO20240529:各资料没看见写这条,是基于CMS推测的
2. -XX:InitiatingHeapOccupancyPercent(IHOP,默认45%)达到后进行Mixed GC,即当整个堆占用超过45%时,就会触发;
回收过程
主要使用复制算法。回收所有的Young区和部分Old区以及大对象区。
注意Concurrent Marking Cycle Phases和MixedGC区别:根据官方文档,Concurrent Marking Cycle Phases包含Global Concurrent Marking和多次MixedGC,而大多资料只提现了MixedGC,相对笼统。
当达到MixedGC触发条件后,会执行Global Concurrent Marking统计出回收收益高的老年代Region,然后开始一系列连续的Mixed GC。
需要注意的是Global Concurrent Marking不是GC过程,它只负责标记、统计出收集收益高的老年代region。参考:小米Talos GC性能调优实践
一系列连续的Mixed GC中,GC的最大次数和单次回收的空间大小可以通过参数-XX:G1MixedGCCountTarget和-XX:G1OldCSetRegionThresholdPercent进行配置。当GC次数超过最大值,或者可回收空间的比例小于参数-XX:G1HeapWastePercent,也会中断MixedGC周期。
MixedGC的步骤(综合了Global Concurrent Marking和Mixed GC,也是大多资料里的体现,更细化的步骤参见官方文档和GC日志
1,初始标记(Initial Marking)(STW)
通常耗时非常短。它标记出从GC Roots直接可达的对象。
2,并发标记(Concurrent Marking):
这个阶段与应用程序线程并发执行,通过递归地追踪所有可达的对象,如果此时YoungGC执行了,并发标记会被中断。
3,最终标记(Final Marking)(STW)
处理在并发标记过程中新产生的对象引用关系,同CMS的重新标记。G1使用STAB算法来进行,速度会被CMS快很多。此时也会将空的区域直接回收。
4,筛选回收(Live Data Counting and Evacuation)(STW)
G1会根据每个Region的垃圾堆积情况和回收价值进行排序(根据衰减平均值理论计算),会优先选择存活率最少的区域来回收,因为这样回收起来更容易。
回收过程包括将存活的对象复制到新的Region,并更新相关的引用。这个阶段可能会涉及到对象的整理和压缩,以减少内存碎片。
③  Full GC
何时触发
1,MinorGC时,老年代没有足够的内存提供给晋升对象,将会触发FullGC,对应日志to-space exhausted。(TODO20240529:结合MixedGC触发条件,这条可能是先触发MixedGC?但结合CMS,如果JVM分配内存压力太大,也有可能直接FullGC
2,MixedGC时拷贝到新region的时候,如果没有足够的空region能够承载拷贝对象就会触发Full GC。
回收过程
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)。
FullGC和MixedGC在执行过程上基本是一致。

5)卡表

全局卡表是一个数组, 记录Region对外的引用。
每个Region被分成了若干个大小为512字节的卡页,每个卡页对应全局卡表中的一个元素(标记项)。如果该卡页中有引用指向了待回收区域的对象,卡表数组对应的元素将被置为1(dirty),没有则置为0。
当发生写操作时, 通过写屏障机制来更新卡表中对应的标记项。这样,在GC时,我们只需要扫描那些被标记为dirty的卡页所对应的Region即可快速找到所有老年代到新生代的引用关系。参考: JVM之记忆集和卡表_卡表和记忆集-CSDN博客G1垃圾回收器深入探索——卡表、记忆集和SATB算法_g1 satb rset cset-CSDN博客

6)RSet

  • RSet是一个HashTable( TODO20240529:好多地方都讲RSet通过卡表来实现?), 记录别的Region对本Region的引用;
  • 每个Region都有一个RSet, RSet记录了其它Region对本Region的引用关系,并标记这些指针分别在哪些卡页的范围内,Key是别的Region的起始地址,Value是全局卡表数组的Index集合;
  • 在进行垃圾标记的时候,会从GC Roots和RSet都去遍历,确保该区域中所有存活的对象都会被标记到;
  • RSet的维护主要来源两个方面: 写屏障(Write Barrier)(类AOP)和并发优化线程(Concurrent refinement threads)。
  • Rset记录的关系有两种:

7)CSet

G1收集器中的CSet(Collection Set)是一组被选中进行垃圾回收的Region的集合,据此 就可以很好的支持增量回收的特性。
当YGC或MixedGC时,CSet中的所有Region都将被释放。

8)SATB(G1)和增量更新(CMS)

并发标记阶段的算法,意图修正并发阶段引用的更新。
CMS和G1修正并发阶段引用更新的实现方案:
CMS:写屏障 + 增量更新
G1:写屏障 + SATB

9)G1调优

-XX:InitiatingHeapOccupancyPercent(整个堆占用百分比,默认45%),如果没有大的cpu负载压力,可以适当降低这个值来提前开始MixedGC,有利于防止年轻代晋升老年代失败(老年代容量不足)而触发Full GC;但太小可能会导致GC频繁。
-XX:MaxGCPauseMills(目标暂停时间),太大可能导致回收不及时,过多对象进入老年代;太小可能会回收太频繁,一次回收的空间过少。

10)参考

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值