作者:京东科技 康志兴
1 前言
随着 Java 的进化过程,涌现出各种不同的垃圾回收器,从串行执行到并行执行,从高吞吐到低提早,终极目标就是让开发人员专一于程序的代码书写而无需关注内存治理。
JDK 晚期呈现的垃圾回收器通常独自作用于不同分代,到前期呈现的 G1 开始,才能够进行全区域收集。
对于垃圾回收器的基础知识请翻看前一篇:从原理聊 JVM(一):染色标记和垃圾回收算法
2 串行收集器(Serial)
比拟老的收集器,单线程,所收集时必须暂停利用的工作线程,直到收集完结。但和其余收集器的单线程相比更加 简略、高效。
作用于新生代的收集器叫 Serial
,采纳 标记复制
算法;作用于年轻代的收集器叫 Serial Old
,采纳 标记整顿
算法。
3 并行收集器(Parallel)
多条垃圾收集线程并行工作,在多核 CPU 下效率更高,但利用线程依然处于期待状态。
并行收集器也分为 ParNew
和Parallel Old
。能够了解为它们就是 Serial 和 Serial Old 的多线程并行版本,甚至局部代码进行了复用。
ParNew 较为风行的起因是因为除了 Serial 只有它能和 CMS 搭配应用。但自 JDK9 开始,因为更先进的 G1 的呈现,官网间接勾销了独自指定 ParNew 的参数-XX:+UseParNewGC
,使其并入了 CMS 收集器,成为它专门解决新生代的组成部分。
而 Parallel Old 则搭配新生代收集器 ParallelScavenge 成为货真价实的“吞吐量优先”的搭配组合。
4 ParallelScavenge
ParallelScavenge 收集器是面向新生代的垃圾回收器,它和 ParNew 其实十分相似,应用标记复制算法并行收集。区别在于二者关注点不同,ParalletScavenge 的指标是达到一个 可管制 的吞吐量(Throughput),更高的吞吐量意味着最大限度的应用处理器的资源来缩短整体的垃圾回收工夫。ParalletScavenge 有两个重要参数:
•-XX:MaxGCPauseMillis
收集器将尽力保障内存回收破费的工夫不超过用户设定值。但这是以就义吞吐量为代价的,要求用更短的工夫来实现垃圾收集,那么零碎就须要升高新生代大小,新生代变小了天然垃圾回收会更加频繁,每次垃圾回收都有很多必要工作(比方期待所有线程达到平安点),那么更频繁的垃圾回收就导致了整体吞吐量的升高。
•-XX:GCTimeRatioGCTimeRatio
是垃圾收集工夫占总工夫的比率,换句话说:其示意运行用户代码工夫是 GC 运行工夫的 X 倍。比方默认为 99,则垃圾收集工夫占比应该 1 /(1+99)。这个数越低,运行用户代码工夫占比越低。
ParallelScavenge 收集器还能够通过参数 (-XX:+UseAdaptiveSizePolicy
) 来激活 自适应调节策略。激活后,就不须要人工指定新生代的大小(Xmn)、Eden 与 Survivor 区的比例(XX:SurvivorRatio)、降职年轻代对象大小(XX:PretenureSizeThreshold)等细节参数了,虚构机会依据以后零碎的运行状况收集性能监控信息,动静调整这些参数以提供最合适的进展工夫或者最大的吞吐量。
5 CMS 收集器(Concurrent Mark Sweep)
CMS 收集器是缩短暂停利用工夫(Low Pause)为指标而设计的,最开始 CMS 仅仅是年轻代收集器,起初将 ParNew 并入作为其年老代收集器。
相较上述收集器,CMS 是第一个无需全程 STW 而容许局部阶段并发执行的收集器。
垃圾回收实际上次要是两个阶段:辨认垃圾和回收垃圾,CMS 在这两个阶段别离做了致力来升高进展:
•辨认垃圾
CMS 将标记过程打散,并将次要的染色标记过程和用户线程同步进行,并通过增量更新形式解决了援用切换带来的漏标的问题。
•垃圾回收
CMS 采纳革除算法,相比复制和整顿,革除算法因为仅解决死亡对象所以不须要任何进展。
CMS 运行步骤
具体来说,CMS 整个过程分为 4 个步骤:
1. 初始标记(Initial Mark)[STW]
初始标记只是标记一下 GC Roots 能间接关联到的对象,速度很快。
2. 并发标记(Concurrent Marking)
并发标记阶段是标记可回收对象。
3. 从新标记(Remark)[STW]
从新标记阶段则是为了修改并发标记期间因用户程序持续运作导致标记产生变动的那一部分对象的标记记录,这个阶段暂停工夫比初始标记阶段稍长一点,但远比并发标记工夫短。
CMS 用增量更新来做并发标记,也就是说并发标记过程中,如果某个曾经标记为存活的对象减少了对非存活对象的援用,那么将其标记为灰色,而后在从新标记阶段将这一部分对象从新扫描。
4. 并发革除(Concurrent Sweep)
清理删除掉标记阶段判断的曾经死亡的对象,因为不须要挪动存活对象,所以这个阶段也是能够与用户线程同时并发的。
长处
因为整个过程中耗费最长的并发标记和并发革除过程收集器线程都能够与用户线程一起工作,所以,CMS 收集器内存回收与用户一起并发执行的,大大减少了暂停工夫。
毛病
1. 处理器资源敏感
垃圾回收的线程可能与用户线程同时执行,这样尽管不会导致 STW,然而因为摊派了处理器的计算资源从而导致应用程序变慢,升高了总吞吐量。
2. 内存敏感
当垃圾回收和用户线程在同步运行时产生的垃圾,因为曾经过了标记阶段所以不会标记后革除,这部分垃圾只能等到下一次 GC 时才会被革除,这就是 浮动垃圾 问题。
而且因为垃圾回收和用户线程同步运行,所以不能等堆满了再 GC,而是须要预留一部分内存来保障 GC 过程中用户线程仍有可用内存。为了升高 GC 频率,只能等垃圾攒多一点再触发 GC,那么 GC 时可供用户线程应用的内存就不多了。
如果 GC 尚未完结用户线程分配内存失败,这个状况叫做“并发失败”,这时虚构机会降级应用 Serial Old 来从新进行一次高吞吐的年轻代收集,这样进展工夫就长了。
线上环境应依据理论状况来调整触发 GC 的内存应用阈值,该参数为:-XX:CMSInitiatingOccupancyFraction
。
3. CMS 基于标记革除算法,所以内存碎片过多后,会频繁触发 Full GC,且不可避免。CMS 会在若干次触发后进行一次内存碎片的合并整顿,内存整理过程波及存活对象的挪动,(在 Shenandoah 和 ZGC 呈现前)无奈并发。
6 G1 收集器(Garbage First)
G1 收集器相比上述垃圾回收器有了里程碑式的翻新,它将堆内存划分多个大小相等的独立区域(Region),并且能建设“进展工夫模型”,使暂停工夫可控,并尽量将-XX:MaxGCPauseMillis
(默认 200ms)作为进展指标。依据 Oracle 官网的形容,G1 是一个“软实时”的收集器,只是尽量保障在指标进展工夫内实现垃圾收集工作,但不能确保肯定:
It is important to note that G1 is not a real-time collector. It meets the set pause time target with high probability but not absolute certainty.
能预测的起因是它能防止对整个堆进行全区收集,而是将整个堆分为若干个小的区域(Region),每个 Region 是单次垃圾回收的最小单元。在零碎运行过程中,G1 跟踪各个 Region 里的垃圾沉积价值大小(所取得空间大小以及回收所需工夫),在后盾保护一个优先列表,每次依据容许的收集工夫,优先回收价值最大的 Region,从而保障了再无限工夫内取得更高的收集效率。这也是 Garbage First 名称的由来。
G1 的分代模型
G1 也分为年老代和年轻代,但不是固定划分,而是每个 Region 依据运行状况动静划分。
G1 还有一个非凡的区域叫 Humongous,G1 将超过了一个 Region 容量一半的大对象,都寄存在 Humongous 区域中,如果对象超过了 Region 大小,则寄存在 N 个间断的 Humongous Region 中。G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行对待。
TAMS(Top at mark start)
为了保障垃圾回收过程中的同时 Region 也可能被应用,G1 为每一个 Region 设计了两个名为 TAMS 的指针,别离是 Previous TAMS(PTAMS)、Next TAMS(NTAMS)。在并发标记阶段开始前,TAMS 指针指向 Region 内占用内存的边界。在并发标记阶段中,G1 默认指针之上的对象为存活对象不去进行标记,而对象调配时,用户线程间接在指针之上调配。这就保障了扫描行为和对象调配互不烦扰。
G1 如何断定 Region 的“价值”
G1 运行期间会收集每个 Region 的价值信息,比方回收耗时、记忆集的脏卡数量等,通过计算得出每个 Region 回收的性价比。G1 的进展预测模型就是通过这些信息,找出在用户预期工夫内取得更高回收收益的 Region 组合。
Remembered Sets
G1 堆中的每一个 Region 都有一份Rememberd Set
,也叫RSet
,它的作用就是为每一个 Region 记录哪些 Region 对其含有援用。
RSet 的更新须要线程同步解决,因为对象援用变更十分频繁,如果同步写卡表耗费十分大,所以通常会把更新信息存入队列中再异步更新 RSet,这个队列就叫Dirty Card Queue
。
G1 的垃圾回收过程
当 Eden 中无奈调配对象时,触发 Young GC。
当年老代占比达到 45% 时,期待下一次 Young GC 时进行并发标记。
并发标记完结后马上执行 Mixed GC。
当 Mixed GC 对内存的清理速度赶不上调配新对象的速度时触发 Full GC,G1 的 Full GC 将应用单线程(JDK11 后改为多线程)执行标记整顿算法,所以耗时微小。
G1 的 Young GC
触发机会
当 JVM 无奈在 Eden 区调配对象时。
回收范畴
Eden 区和 Survivor 区
运行过程(所有阶段均 STW)
1. 根扫描
将所有 Eden 区中的 GC Root 和 RSet 记录的内部援用作为扫描存活对象的入口。
2. 更新 RSet
通过 Dirty Card Queue 中的 card 更新 RSet,保障 RSet 能精确反馈老年代对该 Region 是否存在援用。
3. 解决 RSet
将 Eden 区中被 RSet 指向的对象标记为存活对象。
4. 对象复制
判断存活对象的年龄,如果未达到“阈值”,则复制到一个 Surviver 区中,否则复制到 Old 区中。如果 Surviver 空间不够,则将局部对象间接复制到 Old 区中。
5. 解决援用
解决软援用、弱援用、虚援用等,最终清空全副 Eden 区。这时清理过的内存空间没有内存碎片。
G1 的 Mixed GC
触发机会
年轻代占用空间超过整个堆的 45%(可通过参数 -XX:InitiatingHeapOccupancyPercent
进行设置)
事实上,并不会立即触发,而且期待下一次 Young GC,同步进行初始标记步骤。
回收范畴
被并发标记过的 Region,这些 Region 是 G1 通过价值测算动静选中的。
运行过程
1. 初始标记(Initial Marking)[STW]
标记 GC Roots 间接关联的对象,并批改 TAMS 指针的值。值得注意的是,这一阶段并不独自执行,而是在 Minor GC 时同步实现。所以实际上这个阶段没有额定进展。
2. 并发标记(Concurrent Marking)
与用户线程并发执行,顺着 GC Root 递归标记。标记实现后,从新扫描 SATB 记录的有援用变动的对象。如果这时发现空的 Region 则间接将其清空。
3. 从新标记(Remark)[STW]
因为并发标记是并发执行,并发标记完结后,依然存在大量的援用变动的对象,所以在这个阶段能够 STW 来解决这部分遗留的对象。并且开始计算所有 Region 的活跃度。
4. 清理(Clean Up)[STW]
依据用户冀望的进展工夫来制订回收打算,抉择全副是非存活对象的 Old 区和回收收益较高的 Region 退出回收集。清空记忆集。重置曾经被清理的空的 Region(这一步是非 STW 的)。
5. 拷贝(Coping)[STW]
将回收集其中的存活对象复制到空的 Region 中,最初清空这些旧的 Region。
这个阶段的算法和 Young GC 完全一致,但默认分 8 次执行实现(可由参数 -XX:G1MixedGCCountTarget 设置)。所以每次清理的回收集包含 Eden 区、Survivor 区和八分之一的 Old 区。低存活度(垃圾多)的 Region 清理的较快,所以会被 G1 优先回收。
混合回收并不一定要进行 8 次。有一个阈值 -XX :G1HeapWastePercent(默认值为 10%),意思是容许整个堆内存中有 10% 的空间被节约,意味着如果发现能够回收的垃圾占堆内存的比例低于 10%,则不再进行混合回收。
长处
G1 相比拟之前的垃圾回收器最大的变动是通过化整为零的思路,将堆分为若干个小的 Region 来缩小 GC 的范畴,从而达到“低提早”的目标。
并且 G1 的垃圾回收过程采纳标记复制的算法,防止了空间碎片化的问题。
毛病
1. 内存占用较高,因为 G1 分区比 CMS 更多,每个 Region 都须要建设卡表。其中新生代对象变动频繁,又加大了卡表保护的老本。
2.G1 不仅须要通过写前屏障来更新卡表,还须要写后屏障来跟踪并发时的指针变动以实现快照搜索算法(SATB)。这样尽管相比增量更新算法可能缩小并发标记和从新标记阶段的耗费,然而用户程序运行时的计算负载就高了。
3.G1 和 CMS 同样具备“并发回收”的能力,所以垃圾回收的速度如果跟不上用户创立新对象的速度,那么就会触发一个 Full GC 来获取更多内存。通常把冀望进展工夫设置为一两百毫秒或者两三百毫秒会是比拟正当的。
最佳实际
1. 不要设置年老代大小年老代大小该当由 G1 自行管制,设置为固定值将笼罩暂停工夫指标
2. 暂停工夫指标不要过于严苛 G1 为了 Young GC 可能缩短工夫须要缩小 Eden 区的个数,那么 Young GC 就会更加频繁。Mixed GC 想要达到进展指标就须要缩小回收的垃圾数量,如果回收速度低于新对象调配速度将引起 Full GC。
3.CMS 和 G1 的抉择目前在小内存利用上 CMS 的体现大概率依然要会优于 G1,而在大内存利用上 G1 则大多能施展其劣势,这个优劣势的 Java 堆容量平衡点通常在 6GB 至 8GB 之间。
7 总结
在 GC 的抉择上,同样是“没有银弹”,不同的收集器有着各自的特点和实用场景,即便是 Epsilon 也会在特定场合下发挥作用。咱们应针对不同的业务特色和零碎状况抉择最合适的垃圾回收器,而不是一味求新。
参考:
1.《深刻了解 Java 虚拟机》by 周志明
2.Getting Started with the G1 Garbage Collector