本节介绍,为了应用的评估,分析和性能,如何调节G1收集器。
像在G1收集器那一节描述的,G1收集器是分代的和region
化的,也就是整个堆内存被分为一系列大小相等的region
。在启动时,JVM设置region
的大小,根据堆大小的不同,region
的大小可以在1MB
到32MB
之间变动,region
的数量最多不超过2048个
。Eden区、Survivor区、老年代是这些region
的逻辑集合,它们并不是连续的。
G1收集器可以设置暂停时间目标,收集器将尽可能的达到这个目标(软实时)。在进行年轻代收集时,G1调整它的年轻代(Eden 和 Survivor)以达到软实时的目的;在混合收集时,G1根据三个指标来调整它收集的老年代region
的数量,一是:混合收集的目标数量,二是:堆中每个region
中存活对象的百分比,三是:整个堆可允许的堆内存浪费百分比。
G1将存活对象从一组或多组region
中,通过增量并行的方式,复制到一个或多个不同的新region
来减少堆内存碎片,以达到压缩的目的。目标是,从那些包含最可能多的回收空间的region
开始,尽可能多地回收堆空间,同时尝试不超过暂停时间目标(垃圾优先)。
G1收集器使用独立的Remembered Sets(RSets)
来跟踪对region
的引用。独立的RSets
支持并行和独立的region
集合,因为只有相关region
的RSet必须被扫描,而不是整个堆。G1使用post-write barrier
(写后屏障)来记录对堆的更改并更新RSets。
1、垃圾收集的阶段
除了evacuation
(疏散、转移)阶段(这个阶段包含年轻代的STW和混合垃圾收集),G1收集器也具有并行、并发和多阶段的标记回收阶段,G1收集器使用snapshot-at-the-beginning (SATB)
算法,这种算法是在标记开始时获取堆中存活对象的一个快照。存活对象集还包括自标记周期开始以来分配的对象。G1标记算法使用pre-write barrier
来记录和标记作为逻辑快照的那部分对象。
2、年轻代垃圾收集
G1收集器使用eden region
来满足大多数内存分配需求,在年轻代的垃圾收集过程中,G1收集器,依赖上一次的垃圾收集,来获取需要被收集那些eden region
和survivor region
。eden region
和survivor region
中的存活对象被复制或者转移到新的region
集合中,对于一个特定的对象,它的目标region
取决于对象的年龄,年龄充分大的对象将被转移到old region
中(也就是对象被提升为老年代对象);否则,对象将被转移到survivor region
中,并且也被记录在下一次垃圾收集(可能是年轻代垃圾收集,也可能是混合垃圾收集)的CSet
中。
3、混合垃圾收集
在成功完成并发标记之后,G1收集器会从年轻代垃圾收集切换到混合垃圾收集。在混合垃圾收集过程中,G1选择性的将一些old region
添加到将被收集的eden region
和survivor region
的集合中。具体有多少old region
被添加到待收集集合中,是由一个数字标识控制的(参见下面的建议一节)。在G1收集了足够数量的old region
之后(通过多次的混合收集),G1恢复到年轻代垃圾收集,直到下一个标记周期完成。
4、标记周期中的各个阶段
标记周期分为以下几个阶段:
1.初始标记(Initial marking):初始标记只标记GC Roots
能直接关联到的对象,这个阶段是以正常的年轻代垃圾收集为基础的,需要STW;
2.root region
扫描(Root region scanning):G1扫描在初始标记阶段被标记的survivor region
,从中获取old region
的引用并且标记相关的对象,这个阶段和应用程序并发运行,不需要STW,并且,该阶段必须在下一次需要STW的年轻代垃圾收集之前完成;
3.并发标记(Concurrent marking):G1收集器遍历整个堆以寻找可达对象,这个阶段和应用程序并发执行,并且可以被需要STW是年轻代垃圾收集中断;
4.重新标记(Remark):这个阶段需要STW,该阶段是为了修正在并发标记期间因应用程序继续运作而导致标记产生变动的那一部分标记记录。G1释放SATB
缓冲区,跟踪为访问的存活对象,并执行引用处理;
5.清理阶段(Cleanup):在最后阶段,G1会执行计算和RSet
的清理工作(需要STW),在计算期间,G1标识完全空闲的region
和混合垃圾收集的候选region
。清理阶段,重置空闲region
并将其返回到空闲列表,这个过程是部分并发的。
5、重要的默认值
G1收集器是一个自适应的垃圾收集器,它的默认值无需修改就可以高效的工作,下表列出了一下重要的参数和其默认值:
参数及其默认值 | 说明 |
---|---|
-XX:G1HeapRegionSize = n | 设置region 的大小,该值为2的幂,范围为1MB到32 MB,目标是根据最小Java堆大小,将堆分成大约2048个region 。 |
-XX:MaxGCPauseMillis = 200 | 设置最大停顿时间,默认值为200毫秒。 |
-XX:G1NewSizePercent = 5 | 设置年轻代占整个堆的最小百分比,默认值是5,这是个实验参数,如果设置该值,将覆盖默认参数 -XX:DefaultMinNewGenPercent。(JVM build > 23) |
-XX:G1MaxNewSizePercent = 60 | 设置年轻代占整个堆的最大百分比,默认值是60,这是个实验参数,如果设置该值,将覆盖默认参数 -XX:DefaultMaxNewGenPercent。(JVM build > 23) |
-XX:ParallelGCThreads = n | 设置STW的垃圾收集线程数,当逻辑处理器数量小于8时,n的值与逻辑处理器数量相同;如果逻辑处理器数量大于8个,则n的值大约为逻辑处理器数量的5/8,大多数情况下是这样,除了较大的SPARC系统,其中n的值约为逻辑处理器的5/16。 |
-XX:ConcGCThreads = n | 设置并行标记线程的数量,设置n大约为ParallelGCThreads参数值的1/4。 |
-XX:InitiatingHeapOccupancyPercent = 45 | 设置触发标记周期的Java堆占用阈值,默认值为45。 |
-XX:G1MixedGCLiveThresholdPercent = 85 | 设置要包含在混合垃圾收集周期中的old region 的占用阈值,默认值为85。这是个实验参数,如果设置该值,将覆盖默认参数 -XX:G1OldCSetRegionLiveThresholdPercent。(JVM build > 23) |
-XX:G1HeapWastePercent=5 | 设置浪费的堆内存百分比,当可回收百分比小于浪费百分比时,JVM就不会启动混合垃圾收。(就是设置垃圾对象占用内存百分比的最大值)。(JVM build > 23) |
-XX:G1MixedGCCountTarget = 8 | 设置在标记周期完成之后混合收集的数量,以维持old region (也就是老年代)中,最多有G1MixedGCLiveThresholdPercent的存活对象。默认值为8,混合收集的数量将维持在这个值之内。(JVM build > 23) |
-XX:G1OldCSetRegionThresholdPercent = 10 | 设置在一次混合收集中被收集的old region 数量的上线,默认值是整个堆的10%。(JVM build > 23) |
-XX:G1ReservePercent = 10 | 设置预留空闲内存百分比,以降低内存溢出的风险。默认值为10%。增加或减少百分比时,请确保将总Java堆调整相同的量。(JVM build > 23) |
表格中的 JVM build > 23
表示该参数只有在Java HotSpot VM build 大于 23时才能生效,小于或等于都不能生效
6、如果解锁实验性参数
要使用实验性参数,首先需要解锁,使用参数-XX:+UnlockExperimentalVMOptions
解锁实验性参数。
7、建议
当你在进行G1垃圾收集器调优的时候,请记住以下建议:
1.年轻代大小:避免使用-Xmn
或者其他参数比如-XX:NewRatio
设置年轻代大小,固定年轻代的大小将覆盖暂停目标时间的设置;
2.暂停时间:在进行垃圾收集器调优时,总是存在延迟时间和吞吐量的权衡,G1收集器是增量式垃圾收集器,具有统一的暂停时间,但在应用线程上的开销更大。G1收集器的吞吐量目标是90%的应用线程执行时间,10%的垃圾收集时间,与并行收集器相比,并行收集器的吞吐量目标是99%的应用线程时间,1%的垃圾收集时间。因此,当你在对G1的吞吐量进行调优的时候,增大你的暂停时间目标,设置太小的暂停时间,意味着你愿意承担吞吐量降低的风险。当你在对G1进行响应时间调优的时候,你设置你期望的软实时目标,G1收集器将尽可能的达到该目标,当然,设置响应时间也是又副作用的,吞吐量会响应的受到影响;
3.混合垃圾收集调优:当在进行混合垃圾收集调优的时候,请使用下面几个参数进行调试:-XX:InitiatingHeapOccupancyPercent
(改变标记阈值)、-XX:G1MixedGCLiveThresholdPercent 和 -XX:G1HeapWastePercent
(改变混合收集决策)、-XX:G1MixedGCCountTarget 和 -XX:G1OldCSetRegionThresholdPercent
(调整old region
集合 CSet)。
8、溢出和耗尽日志。
当你在G1的GC日志中看到to-space overflow
或者to-space exhausted
的时候,表示G1没有足够的内存使用的(可能是survivor
区不够了,可能是老年代不够了,也可能是两者都不够了),这时候表示Java堆占用大小已经达到了最大值。比如:
924.897: [GC pause (G1 Evacuation Pause) (mixed) (to-space exhausted), 0.1957310 secs]
924.897: [GC pause (G1 Evacuation Pause) (mixed) (to-space overflow), 0.1957310 secs]
为了解决这个问题,请尝试做以下调整:
1.增加预留内存:增大参数-XX:G1ReservePercent
的值(相应的增加堆内存)来增加预留内存;
2.更早的开始标记周期:减小-XX:InitiatingHeapOccupancyPercent
参数的值,以更早的开始标记周期;
3.增加并发收集线程数:增大-XX:ConcGCThreads
参数值,以增加并行标记线程数。
9、大对象和大对象分配
对G1而言,大小超过region
大小50%的对象将被认为是大对象,这种大对象将直接被分配到老年代的humongous regions
中,humongous regions
是连续的region
集合, StartsHumongous
表记集合从那里开始,ContinuesHumongous
标记连续集合。在分配大对象之前,将会检查标记阈值,如果有必要的话,还会启动并发周期。死亡的大对象会在标记周期的清理阶段和发生Full GC的时候被清理。为了减少复制开销,任何转移阶段都不包含大对象的复制。在Full GC时,G1在原地压缩大对象。因为每个独立的humongous regions
只包含一个大对象,因此从大对象的结尾到它占用的最后一个region
的结尾的那部分空间时没有被使用的,对于那些大小略大于region
整数倍的对象,这些没有被使用的内存将导致内存碎片化。如果你看到因为大对象的分配导致不断的启动并发收集,并且这种分配使得老年代碎片化不断加剧,那么请增加-XX:G1HeapRegionSize
参数的值,这样的话,大对象将不再被G1认为是大对象,它会走普通对象的分配流程。
参考资料
1.《深入理解Java虚拟机:JVM高级特性与最佳实践》(第2版);
2.Java HotSpot Virtual Machine Garbage Collection Tuning Guide Release 8;
3.Java垃圾收集必备手册。