我发现,目前不少外部资料对G1的介绍大多还停留在JDK 7或更早期的实现,很多结论已经存在较大偏差,甚至一些过去的GC选项已经不再推荐使用。所以,今天我会选取新版JDK中的默认G1 GC作为重点进行详解,并且我会从调优实践的角度,分析典型场景和调优思路。下面我们一起来更新下这方面的知识。

今天我要问你的问题是,谈谈你的GC调优思路?

典型回答

谈到调优,这一定是针对特定场景、特定目的的事情, 对于GC调优来说,首先就需要清楚调优的目标是什么?从性能的角度看,通常关注三个方面,内存占用(footprint)、延时(latency)和吞吐量(throughput),大多数情况下调优会侧重于其中一个或者两个方面的目标,很少有情况可以兼顾三个不同的角度。当然,除了上面通常的三个方面,也可能需要考虑其他GC相关的场景,例如,OOM也可能与不合理的GC相关参数有关;或者,应用启动速度方面的需求,GC也会是个考虑的方面。

基本的调优思路可以总结为:

考点分析

今天考察的GC调优问题是JVM调优的一个基础方面,很多JVM调优需求,最终都会落实在GC调优上或者与其相关,我提供的是一个常见的思路。

真正快速定位和解决具体问题,还是需要对JVM和GC知识的掌握,以及实际调优经验的总结,有的时候甚至是源自经验积累的直觉判断。面试官可能会继续问项目中遇到的真实问题,如果你能清楚、简要地介绍其上下文,然后将诊断思路和调优实践过程表述出来,会是个很好的加分项。

专栏虽然无法提供具体的项目经验,但是可以帮助你掌握常见的调优思路和手段,这不管是面试还是在实际工作中都是很有帮助的。另外,我会还会从下面不同角度进行补充:

知识扩展

首先,先来整体了解一下G1 GC的内部结构和主要机制。

从内存区域的角度,G1同样存在着年代的概念,但是与我前面介绍的内存结构很不一样,其内部是类似棋盘状的一个个region组成,请参考下面的示意图。

region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的region,这点可以从源码heapRegionBounds.hpp中看到。当然这个数字既可以手动调整,G1也会根据堆大小自动进行调整。

在G1实现中,年代是个逻辑概念,具体体现在,一部分region是作为Eden,一部分作为Survivor,除了意料之中的Old region,G1会将超过region 50%大小的对象(在应用中,通常是byte或char数组)归类为Humongous对象,并放置在相应的region中。逻辑上,Humongous region算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代GC的复制算法。

你可以思考下region设计有什么副作用?

例如,region大小和大对象很难保证一致,这会导致空间的浪费。不知道你有没有注意到,我的示意图中有的区域是Humongous颜色,但没有用名称标记,这是为了表示,特别大的对象是可能占用超过一个region的。并且,region太小不合适,会令你在分配大对象时更难找到连续空间,这是一个长久存在的情况,请参考OpenJDK社区的讨论。这本质也可以看作是JVM的bug,尽管解决办法也非常简单,直接设置较大的region大小,参数如下:

-XX:G1HeapRegionSize=<N, 例如16>M

从GC算法的角度,G1选择的是复合算法,可以简化理解为:

我在上一讲曾经介绍过,习惯上人们喜欢把新生代GC(Young GC)叫作Minor GC,老年代GC叫作Major GC,区别于整体性的Full GC。但是现代GC中,这种概念已经不再准确,对于G1来说:

–XX:G1MixedGCLiveThresholdPercent
–XX:G1OldCSetRegionThresholdPercent

从G1内部运行的角度,下面的示意图描述了G1正常运行时的状态流转变化,当然,在发生逃逸失败等情况下,就会触发Full GC。

G1相关概念非常多,有一个重点就是Remembered Set,用于记录和维护region之间对象的引用关系。为什么需要这么做呢?试想,新生代GC是复制算法,也就是说,类似对象从Eden或者Survivor到to区域的“移动”,其实是“复制”,本质上是一个新的对象。在这个过程中,需要必须保证老年代到新生代的跨区引用仍然有效。下面的示意图说明了相关设计。

G1的很多开销都是源自Remembered Set,例如,它通常约占用Heap大小的20%或更高,这可是非常可观的比例。并且,我们进行对象复制的时候,因为需要扫描和更改Card Table的信息,这个速度影响了复制的速度,进而影响暂停时间。

描述G1内部的资料很多,我就不重复了,如果你想了解更多内部结构和算法等,我建议参考一些具体的介绍,书籍方面我推荐Charlie Hunt等撰写的《Java Performance Companion》。

接下来,我介绍下大家可能还不了解的G1行为变化,它们在一定程度上解决了专栏其他讲中提到的部分困扰,如类型卸载不及时的问题。

-XX:+UseStringDeduplication

注意,这种排重虽然可以节省不少内存空间,但这种并发操作会占用一些CPU资源,也会导致Young GC稍微变慢。

G1的类型卸载有什么改进吗?很多资料中都谈到,G1只有在发生Full GC时才进行类型卸载,但这显然不是我们想要的。你可以加上下面的参数查看类型卸载:

-XX:+TraceClassUnloading

幸好现代的G1已经不是如此了,8u40以后,G1增加并默认开启下面的选项:

-XX:+ClassUnloadingWithConcurrentMark

也就是说,在并发标记阶段结束后,JVM即进行类型卸载。

-XX:InitiatingHeapOccupancyPercent

在JDK 9之后的G1实现中,这种调整需求会少很多,因为JVM只会将该参数作为初始值,会在运行时进行采样,获取统计数据,然后据此动态调整并发标记启动时机。对应的JVM参数如下,默认已经开启:

-XX:+G1UseAdaptiveIHOP

当然,还有很多其他的改变,比如更快的Card Table扫描等,这里不再展开介绍,因为它们并不带来行为的变化,基本不影响调优选择。

前面介绍了G1的内部机制,并且穿插了部分调优建议,下面从整体上给出一些调优的建议。

首先,建议尽量升级到较新的JDK版本,从上面介绍的改进就可以看到,很多人们常常讨论的问题,其实升级JDK就可以解决了。

第二,掌握GC调优信息收集途径。掌握尽量全面、详细、准确的信息,是各种调优的基础,不仅仅是GC调优。我们来看看打开GC日志,这似乎是很简单的事情,可是你确定真的掌握了吗?

除了常用的两个选项,

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps

还有一些非常有用的日志选项,很多特定问题的诊断都是要依赖这些选项:

-XX:+PrintAdaptiveSizePolicy // 打印G1 Ergonomics相关信息

我们知道GC内部一些行为是适应性的触发的,利用PrintAdaptiveSizePolicy,我们就可以知道为什么JVM做出了一些可能我们不希望发生的动作。例如,G1调优的一个基本建议就是避免进行大量的Humongous对象分配,如果Ergonomics信息说明发生了这一点,那么就可以考虑要么增大堆的大小,要么直接将region大小提高。

如果是怀疑出现引用清理不及时的情况,则可以打开下面选项,掌握到底是哪里出现了堆积。

-XX:+PrintReferenceGC

另外,建议开启选项下面的选项进行并行引用处理。

-XX:+ParallelRefProcEnabled

需要注意的一点是,JDK 9中JVM和GC日志机构进行了重构,其实我前面提到的PrintGCDetails已经被标记为废弃,而PrintGCDateStamps已经被移除,指定它会导致JVM无法启动。可以使用下面的命令查询新的配置参数。

java -Xlog:help

最后,来看一些通用实践,理解了我前面介绍的内部结构和机制,很多结论就一目了然了,例如:

-XX:G1NewSizePercent

降低其最大值同样对降低Young GC延迟有帮助。

-XX:G1MaxNewSizePercent

如果我们直接为G1设置较小的延迟目标值,也会起到减小新生代的效果,虽然会影响吞吐量。

还记得前面说的,部分Old region会被包含进Mixed GC,减少一次处理的region个数,就是个直接的选择之一。
我在上面已经介绍了G1OldCSetRegionThresholdPercent控制其最大值,还可以利用下面参数提高Mixed GC的个数,当前默认值是8,Mixed GC数量增多,意味着每次被包含的region减少。

-XX:G1MixedGCCountTarget

今天的内容算是抛砖引玉,更多内容你可以参考G1调优指南等,远不是几句话可以囊括的。需要注意的是,也要避免过度调优,G1对大堆非常友好,其运行机制也需要浪费一定的空间,有时候稍微多给堆一些空间,比进行苛刻的调优更加实用。

今天我梳理了基本的GC调优思路,并对G1内部结构以及最新的行为变化进行了详解。总的来说,G1的调优相对简单、直观,因为可以直接设定暂停时间等目标,并且其内部引入了各种智能的自适应机制,希望这一切的努力,能够让你在日常应用开发时更加高效。

一课一练

关于今天我们讨论的题目你做到心中有数了吗?今天的思考题是,定位Full GC发生的原因,有哪些方式?

请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。

你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。