G1(Garbage-First)是 Java 虚拟机(JVM)中一款面向服务端应用的垃圾收集器,旨在为大内存机器提供高吞吐量且停顿时间可预测的内存回收能力。
jdk9及以后垃圾优先垃圾回收器是默认的垃圾回收器,因此通常无需执行任何额外操作。您可以通过 -XX:+UseG1GC命令行显式启用它。
G1 计划作为并发标记清除收集器 (CMS) 的长期替代方案。与 CMS 相比,G1 的一些差异使其成为更优的解决方案。其中一个区别在于 G1 是一种压缩式收集器。G1 的压缩程度足以完全避免使用细粒度的空闲列表进行内存分配,而是依赖于区域。这大大简化了收集器的部分功能,并基本消除了潜在的碎片问题。此外,与 CMS 收集器相比,G1 提供了更可预测的垃圾回收暂停时间,并允许用户指定所需的暂停目标。
核心设计理念:基于 Region 的内存划分
与传统垃圾收集器(如 CMS)将堆空间划分为连续的年轻代和老年代不同,G1 将整个 Java 堆划分为约 2048 个大小相等且独立的 Region(区域)。
角色动态分配:每个 Region 可以根据需要扮演 Eden、Survivor、Old 或 Humongous(存储巨型对象)的角色。
非连续性:逻辑上的新生代和老年代由一系列不连续的 Region 组成,这种设计极大地提高了内存分配和回收的灵活性。
较老的垃圾回收器(串行、并行、CMS)都将堆分为三个部分:新生代、老年代和具有固定内存大小的永久代。
G1采用不同的方法:
region 的大小是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数,JVM 会尽量划分 2048 个左右、同等大小的 region。当然这个数字既可以手动调整,G1 也会根据堆大小自动进行调整。
在 G1 实现中,年代是个逻辑概念,具体体现在,一部分 region 是作为 Eden,一部分作为 Survivor,除了意料之中的 Old region,G1 会将超过 region 50% 大小的对象(在应用中,通常是 byte 或 char 数组)归类为 Humongous 对象,并放置在相应的 region 中。逻辑上,Humongous region 算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代 GC 的复制算法。
垃圾回收机制
G1 的名字源于其核心策略:优先回收垃圾最多的区域(Garbage First)。它主要有以下三种回收模式:
- Young GC(新生代回收):当 Eden 区满时触发,将存活对象从 Eden 和 Survivor 移动到新的 Survivor 或老年代 Region 中。
- Mixed GC(混合回收):在老年代占用率达到阈值(默认 45%)时触发。它不仅回收所有的新生代,还会回收一部分老年代 Region。
- Full GC:如果内存分配速度远快于回收速度,G1 会退化为 Full GC,这通常是单线程或并发性较低的回收,应尽量通过调优避免。
工作流程(并发标记周期)
为了实现短时间的停顿,G1 在回收老年代时采用了复杂的并发标记过程:
- 初始标记 (Initial Mark):标记 GC Roots 直接关联的对象(需 STW 停顿,通常伴随 Young GC 进行)。
- 并发标记 (Concurrent Marking):从 GC Roots 开始对堆中对象进行可达性分析,此过程与应用线程并发运行。
- 最终标记 (Remark):处理并发标记期间遗留的少量变动对象(需 STW 停顿)。
- 筛选回收 (Cleanup/Evacuation):根据各 Region 的回收价值和成本排序,根据用户设定的期望停顿时间制定回收计划,并将存活对象复制到空 Region 中(需 STW 停顿)。
关键技术支撑
记忆集 (Remembered Sets, RSet):每个 Region 维护一个 RSet,记录“谁引用了我”,从而实现独立回收某个 Region 而无需扫描整个堆。
可预测的停顿时间:用户可以通过参数 -XX:MaxGCPauseMillis 设置期望的最大 GC 停顿时间(默认 200ms),G1 会尽力在这个时间内完成回收工作。
G1分步指南
(1) G1堆结构
堆是将一块内存区域分割成许多固定大小的区域。
区域大小由 JVM 在启动时选择。JVM 通常目标区域大小约为 2000 个,大小从 1MB 到 32MB 不等。
(2) G1堆分配
实际上,这些区域被映射到Eden、Survivor和老年代 Region空间的逻辑表示中。
(3) G1 的年轻一代
堆内存被划分成大约 2000 个区域。最小大小为 1MB,最大大小为 32MB。蓝色区域存放老年代对象,绿色区域存放新生代对象。
请注意,这些区域不需要像以前的垃圾收集器那样是连续的。
(4) G1 Young GC
存活对象会被迁移(即复制或移动)到一个或多个存活区。如果达到老化阈值,部分对象会被提升到老年代区。
(5) G1 Young GC 结束
存活对象已被疏散到Survivor区域或老年代区域。
(6) 初始标记阶段 (Initial Marking Phase)
对存活对象的初始标记是借助新生代垃圾回收实现的。在日志中,这会被记录为GC pause (young)(inital-mark)。
(7) 同步标记阶段 (Concurrent Marking Phase)
如果在备注阶段发现空区域(以“X”标记),则立即将其移除。此外,还会计算用于判断存活状态的“统计”信息。
(8) 备注阶段 (Remark Phase)
空区域已被移除并回收。现在将计算所有区域的活跃度。
(9) 复制/清理阶段 (Copying/Cleanup Phase)
G1 选择“存活率”最低的区域,也就是那些可以最快收集的区域。然后,这些区域会与新生代垃圾回收同时进行收集。这在日志中会以 表示[GC pause (mixed)]。因此,新生代和老代垃圾回收同时进行。
(10) 复制/清理阶段之后
选定的区域已被收集并压缩成图中所示的深蓝色区域和深绿色区域。
G1收集器参数
| 参数 | 描述 |
|---|---|
| -XX:+UseG1GC | 使用 G1 (Garbage First) 垃圾收集器 |
| -XX:MaxGCPauseMillis=n | 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标. |
| -XX:InitiatingHeapOccupancyPercent=n | 启动并发GC周期时的堆内存占用百分比. G1收集器用它来触发并发GC周期,基于整个堆的使用率, 默认值为 45. |
| -XX:NewRatio=n | 新生代与老生代(new/old generation)的大小比例,默认值为 2. |
| -XX:SurvivorRatio=n | eden/survivor 空间大小的比例(Ratio). 默认值为 8. |
| -XX:MaxTenuringThreshold=n | 提升年老代的最大临界值(tenuring threshold). 默认值为 15. |
| -XX:ParallelGCThreads=n | 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同. |
| -XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同. |
| -XX:G1ReservePercent=n | 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10. |
| -XX:G1HeapRegionSize=n | 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb. |
参考资料
Getting Started with the G1 Garbage Collector
Garbage-First (G1) Garbage Collector
康杨 垃圾回收器:为什么G1被叫做GC中的王者?
– end –