导航
- 引言
- 一、G1 介绍
- 1.1 适用场景
- 1.2 设计初衷
- 1.3 关注焦点
- 1.4 工作模式
- 1.5 堆的逻辑结构
- 1.6 主要收集目标
- 1.7 停顿预测模型
- 1.8 拷贝和压缩
- 1.9 与 CMS 和 Parallel 收集器的比较
- 1.10 固定停顿目标
- 二、堆的逻辑分区
- 2.1 region
- 2.2 CSet
- 2.3 RSet
- 2.4 Card Table
- 三、G1 的工作原理
- 3.1 YGC
- 3.2 Mixed GC
- 3.3 工作流程
- 四、常用的G1调优参数和建议
- 4.1 G1常见命令行参数及其默认值
- 4.2 调优建议
- 4.3 to-space 的溢出和耗尽
- 4.4 Humongous 对象及其分配
引言
本文针对 Hotspot 虚拟机的 G1 垃圾收集器进行总结和归纳,适用于JDK 8。
文章内容以 Garbage-First Garbage Collector 和 Garbage-First Garbage Collector Tuning 两篇官方文章为主,结合 JVM面试必问:G1垃圾回收器 技术社区分享汇总。
一、G1 介绍
G1 是 Hotspot 虚拟机在 jdk 8 之后正式(java7处于试验阶段)推出的一款针对多核CPU、大内存的服务器端应用程序提供服务的垃圾收集器。
1.1 适用场景
通用需要:大堆空间(>6G)、低延迟(< 0.5s)
业务需要:堆中有 超过50% 的存活对象,对象的分配率和提升率差异很大。
1.2 设计初衷
实现高吞吐量的同时获得尽可能短暂的STW停顿。
1.3 关注焦点
为用户提供一种更大的堆且低延迟的解决方案。例如,6G的堆,且延迟基本可以稳定在小于0.5秒。
1.4 工作模式
面向整个堆空间进行GC,某些阶段如并发标记,可与工作线程并发执行。
1.5 堆的逻辑结构
整个堆被分成一个个大小相等且物理地址连续的 region(分区)。region大小在1M~32M不等(2的幂),取决于堆的大小,但总数不会超过2048。
为兼容传统的分代机制,eden、survivor、old 这些年代概念依然存在,但在G1中是以 region 为单位,分散在堆中(相同或不同的年代之间不一定连续)。
1.6 主要收集目标
Garbage First 垃圾优先。
通过全局的并发标记,G1可以得知哪些区域大部分都是垃圾,G1会首先收集和压缩这些region。优先收集垃圾更多的 region,就意味着可以以更短的时间获得更大的收益。
1.7 停顿预测模型
G1会使用一种称为“暂停预测”的模型,来评估需要收集多少region,从而限制停顿时间(STW)。
1.8 拷贝和压缩
对于内存碎片问题,G1在GC的过程中会将一个或多个 region中的存活对象拷贝、压缩到一个新的region 中,以减少堆空间的碎片化。
这项操作必须是 STW的,因为要拷贝到新的region,如果应用程序不停的分配对象,很明显会严重干扰拷贝压缩的过程。
这个过程会在多处理器上并行执行以降低停顿时长。因此,每次GC,G1都在努力减少内存碎片。
1.9 与 CMS 和 Parallel 收集器的比较
G1要优于这两种收集器。
CMS 默认是不进行内存压缩的,所以会产生严重的内存碎片问题;
Parallel收集器会对整个堆进行数据压缩,这会导致严重的STW。
1.10 固定停顿目标
用户可以设定STW的停顿时间,但G1只是 大概率能够满足而并非绝对 。它会根据之前GC的情况,会构建一个相当精确的“收集成本模型”,以此来决定要收集哪些region以及收集多少region。
a reasonably accurate model of the cost of collecting the regions
二、堆的逻辑分区
2.1 region
上图是Oracle官网给出的G1收集器对整个堆结构的逻辑划分示例。
G1 将整个堆划分为一个个相同大小的 region。它覆盖了一块连续的内存空间,大小在 1M ~ 32M不定,但一定是 2 的幂,默认数量是2048个。
和传统的垃圾收集器,如Serial、Parallel、CMS等等不同,G1只是在逻辑上分代。
也就是说,G1摒弃了传统的大块分代空间的做法,以 region 为单元,将 region标记为年轻代或老年代,这些region共同组成年轻代或老年代的整体大小。而且G1可以随着运行情况和策略的变化动态调整年轻代或老年代region的数量以此来调整对应分代的大小。
2.2 CSet
CSet ,Collection Set ,它是一组可被回收的 region 集合。在后面讲到的 YGC 中,会提到它的使用。CSet 中的 region 既可以是年轻代,也可以是老年代,G1 会将 CSet中存活的对象拷贝、压缩到新的region,CSet 大概占用堆空间的 1% 大小。
2.3 RSet
对于多数分代收集器,YGC 会先收集堆中年轻代的内存空间,这是一种增量收集的方式。G1作为逻辑分代的收集器,同样属于增量式垃圾收集器。
在进行增量收集的时候,收集器需要知道未收集的对象有没有指向正在收集的对象的引用。对于 YGC 来说,通常是老年代指向年轻代。而 RSet 就是这样一种记忆结构,专门用于存储一组其他对象指向当年对象的引用。
对于 G1 而言,RSet 是位于 region 中。之所以设计这样一个结构,就是为了在做 GC 标记的时候,避免全图扫描。这也是 G1 高效 GC 的关键手段之一。
2.4 Card Table
Card Table 是一中特殊类型的 RSet。
A card table is a particular type of remembered set. —— Card Tables and Concurrent Phases
Hotspot 虚拟机使用一组字节数组来实现 Card Table。每个字节代表一个 card,card又对应堆中的一块连续内存地址。如果引用关系发生改变,就将对应的 card 标记为 dirty,在 remark 阶段就只需要扫描这些 dirty card 就可以快速追踪引用发生变化的对象。
三、G1 的工作原理
G1的工作过程是一个非常复杂的流程,包含很多可考虑的因素和条件。
3.1 YGC
YGC 又叫做 young collection 或 Minor GC,会有STW停顿。它是G1收集器的先锋兵,实际上不光是G1,其他收集器在最初发生GC的时候都要先进行 YGC。
Java中的大多数对象都主要分配在堆的 Eden 区,它隶属于年轻代,同时又随着应用程序的运行快速“消亡”(对象不可达),因此 YGC 是回收这些对象内存空间的有效手段。
在 YGC 期间,G1会从 CSet 中获取需要收集的 eden 和 survivor 区。CSet 是 Collection Set 的缩写,它是用于记录需要做垃圾收集的 region集合。G1 通过将活动对象从 eden 和 survivor 区增量并行复制到一个或多个不同的新 region 来实现内存压缩,减少碎片。
不同对象的目标region取决于对象的分代年龄,当达到年龄阈值,就会分配到老年代 region 中,而年龄不足的,会被分配到 survivor region中,并被包含在下一次的YGC 或 Mixed GC的CSet中。
值得一提的是,在STW的YGC期间,还会额外做一些有备后用的操作,如初始标记,这个过程是后续执行并发标记的“必要前戏”,目标是标记出 GC roots 根节点,由于本身就需要STW,因此放在YGC的STW中完成是希望能够充分利用停顿时间,以此达到G1的设计目标——pause time goal。
3.2 Mixed GC
Mixed GC 是在 YGC 的基础之上,选择性的增加 old region 的清理。
在 YGC 的过程中,G1会进行初始标记工作,之后,G1的垃圾回收线程就会进入重要的并发标记环节。
concurrent mark 阶段是与用户线程并发执行的垃圾收集工作,可以被 YGC 的 STW 打断。其目标是标记出所有可达的对象(三色标记算法+RSet)。
Concurrent marking phase: The G1 GC finds reachable (live) objects across the entire heap. This phase happens concurrently with the application, and can be interrupted by STW young garbage collections.
G1的设计目标是尽可能在用户设定的停顿时间之内完成垃圾收集,为了达到这一点,并发执行一部分工作是必要的,但带来的问题就是,并发工作的GC线程势必会占用原本应该去执行用户线程的CPU资源,造成的损失就是吞吐量的下降(官方给出的参考数据是 90%的用户线程时间和 10%的GC时间,而Parallel收集器的数据是 99% 的用户线程时间和 1%的GC时间)。
在并发标记阶段完成后,G1 的 YGC就会切换到 Mixed GC。
Mixed GC 阶段,G1会选择性的将一些 old region 加入到待收集的CSet 中,随 eden、survivor一同被G1清理。当G1清理了足够的 old region后(多次Mixed GC),G1就再次切换回 YGC,直到下次并发标记完成。
总之,G1的GC过程可以用这样一段描述来概括:
一开始G1使用YGC进行年轻代的垃圾清理,拷贝、压缩存活的对象,但当堆内存可用空间渐渐不足,例如活动对象占用超过整个堆的45%这一阈值时,就会触发G1的标记周期,这一过程以并发标记为主,当标记结束后,G1启动 MixedGC,它不仅会清理年轻代,还会选择性的清理一些老年代,当经过了几轮的 MixedGC之后,G1已经收集了足够多的老年代区域,此时堆的空间也不再紧张,G1就会再次切回YGC进行垃圾回收。
3.3 工作流程
YGC 和 Mixed GC 实际上是 G1 的两种工作模式,对于 Mixed GC,完整的工作流程应该包含以下几个阶段。
初始标记(Initial Mark): 这个阶段仅仅只是标记GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但是耗时很短。而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿
并发标记(Concurrent Mark):从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
最终标记(Final Mark): 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
筛选回收(Live Data Counting and Evacuation): 负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个Region来构成会收集,然后把回收的那一部分Region中的存活对象疏散到新的 region中。
四、常用的G1调优参数和建议
G1 是一个自适应的垃圾收集器,一些默认参数足可以高效工作。以下是 G1 相关的控制参数,了解这些参数的含义可以帮助我们更好的理解 G1 收集器。
4.1 G1常见命令行参数及其默认值
参考:Important Defaults
参数和默认值 | 说明 |
---|---|
-XX:G1HeapRegionSize=n | 设置 G1 的region的大小,1M ~ 32M ,2的幂。 目标是以-Xms为准大约有 2048 个 region |
-XX:MaxGCPauseMillis=200 | 设置期望的最大停顿时间。默认为 200ms |
-XX:G1NewSizePercent=5 | 年轻代占总堆的最小百分比。默认 5%。该参数为实验参数,必须在该参数前面设置-XX:+UnlockExperimentalVMOptions 解锁实验参数,才能生效 |
-XX:G1MaxNewSizePercent=60 | 年轻代占总堆最大百分比,应和上一条参数互补。 |
-XX:ParallelGCThreads=n | 设置 STW 时垃圾收集线程数,将 n 设置为逻辑处理器的数量,但最大不要超过 8。如果逻辑处理器数量超过8,可以将 n 设置为逻辑处理器的 5/8 。 |
-XX:ConcGCThreads=n | 设置并行标记的线程数。可以将 n 设置为 ParallelGCThreads 参数的 1/4 。 |
-XX:InitiatingHeapOccupancyPercent=45 | 设置足以触发初始标记的堆占用阈值。默认为总堆大小的 45%。 |
-XX:G1MixedGCLiveThresholdPercent=85 | 设置 Mixed GC 开始回收老年代 region 的堆占用阈值,默认为 85%。这是一个实验性参数,须在该参数前面设置-XX:+UnlockExperimentalVMOptions 解锁实验参数,才能生效。 |
-XX:G1HeapWastePercent=5 | 设置允许浪费的堆空间占比 |
-XX:G1OldCSetRegionThresholdPercent=10 | 设置可以被 Mixed GC 回收的老年代 region 占比上限。默认为 10% |
-XX:G1ReservePercent=10 | 设置预留的空内存大小,以防 to survivor 区溢出。默认为 10%。如果你要调整该参数,请记得将总堆的大小也一同调整。 |
4.2 调优建议
- 年轻代的大小
避免显式设置年轻代的大小,因为 G1 会根据停顿目标自动调整年轻代的分配大小。 - STW限定目标
任何垃圾收集器都存在吞吐量和延迟的权衡。
G1 GC 的吞吐量目标是 90% 的应用程序时间和 10% 的垃圾收集时间。而 Parallel 收集器是 99% 的应用程序时间和 1% 的垃圾收集时间。因此,如果想要优化吞吐量,就必须要降低延迟的标准。 - 调试 Mixed GC
当你在调试 Mixed GC时,可以测试以下这些参数:
-XX:InitiatingHeapOccupancyPercent:改变触发标记周期的阈值
-XX:G1MixedGCLiveThresholdPercent 和 -XX:G1HeapWastePercent:改变 Mixed GC 的决策
-XX:G1MixedGCCountTarget 和 -XX:G1OldCSetRegionThresholdPercent :调整 CSet 中老年代的数量
4.3 to-space 的溢出和耗尽
如果GC日志中出现"to-space overflow"或"to-space exhausted"这样的消息,就表示G1 GC没有足够的内存来存放 survivor 或提升的对象。
这种情况可能是由于堆已经达到了最大值。例如:
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]
可以尝试调整以下参数:
- -XX:G1ReservePercent:调大该参数(以及相应的总堆大小)以增加 “to-space” 的预留空间。
- -XX:InitiatingHeapOccupancyPercent:调小该参数,以更早的触发标记过程。
- -XX:ConcGCThreads:调大该参数,以增加并行标记线程的数量。
4.4 Humongous 对象及其分配
humongous 意为“极大的,硕大的”。对于 G1 来说,只要对象大小超过了 region 的一半,都称为 humongous 对象。
humongous 对象会被直接分配到老年代的 humongous region,有时 humongous 对象可能会跨越多个 region 分区。
在G1分配 humongous 之前,都会检查标记阈值,如果有必要,就会触发初始标记,为 Mixed GC 做准备。
为了减少拷贝的开销,一般不会轻易拷贝压缩 humongous 对象,G1 会在恰当的时机压缩 humongous 对象。
如果发现因为 humongous 对象的分配而频繁触发Mixed GC,并且老年代还在不断飙升,那么可以适当增加 region 的大小,这样,以前的 humongous 对象就有可能不会超过 region 的一半,从而遵循常规分配策略。