1. JVM G1垃圾回收器
1.1. G1 垃圾回收器简介
G1(Garbage-First)是 Java 7 引入,在 Java 9 及以后版本的默认垃圾回收器,专为大内存(8GB~几十GB)、低延迟(可控停顿时间)的场景设计。
1.2. G1 核心设计思路
G1 最大的特点是,将堆内存,分为多个大小相等的 Region。也分新生代和老年代,通过逻辑上来区分。
JVM优化的思路,就是为了尽可能减少Minor GC和Full GC,尽量减少GC带来的系统停顿,避免影响系统处理请求。G1可以帮忙负责达到预期的目标,可以设置预期停顿的时间。
G1 将内存分为多个 Region 后,会追踪每个 Region 里的回收价值(内部有多少垃圾对象,大概需要耗时多久)。然后根据配置的停顿参数及之前回收的情况,会自动选择满足条件的 Region 进行回收。尽量把垃圾回收对系统造成的影响控制在指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象。
在G1中,每一个 Region 可能属于新生代,也可能属于老年代。最开始可能是新生代,垃圾回收后,可能会给老年代使用,由G1自动控制。
如果对象大小超过 Region 的大小,则该对象会横跨多个 Region 来存放。
1.2.1. 设定G1内存大小
G1 的 Region 大小是自动计算出来的。启动时如果发现使用的是G1垃圾回收器(-XX:+UserG1GC
),根据-Xms和-Xmx来设置堆大小,然后会自动用堆大小除以 2048,最多可以有 2048 个 Region,每个 Region 大小必须是2的倍数 (Region大小可以通过手动方式设置:-XX:G1HeapRegionSize
)
新生代最开始 Region 占比为 5%(可以通过-XX:G1NewSizePercent
来设置),随着系统运行,数量占比逐渐增加,新生代占比不会超过60%(可以通过-XX:G1MaxNewSizePercent
来设置)。
1.2.2. 新生代Eden和Survivor
新生代还是分Eden和Survivor区域的<占有不同Region>(新生代参数-XX:SurvivorRatio=8
在G1也会使用,来区分Eden和Survivor占比)。随着对象在新生代里变多,属于新生代的Region会增加,Eden和Survivor对应的Region也会增加。
1.3. G1 工作流程
1.3.1. G1 新生代垃圾回收
G1的新生代也有Eden和Survivor的区分,触发垃圾回收的机制是类似的。在新生代Eden的Region中放新对象,新生代对应的Region会越来越多,直到占据堆大小的最大比例60%。
G1会使用复制算法进行垃圾回收,进入Stop the World
状态,把Eden对应的Region中的存活对象放入S1对应的Region中,然后整体回收Eden对应的Region。
这个过程和之前是有区别的,因为G1可以设定目标GC停顿时间的,就是G1执行GC的时候最多让系统停顿多少时间,可以通过-XX:MaxGCPauseMills
参数来设定,默认值是200ms。
G1会对每个Region追踪回收他需要多少时间、可以回收多少对象。然后自己判定选择回收一部分的Region,保证GC停顿时间控制在指定范围内,同时尽可能多的回收掉一些对象。
1.3.2. 进入老年代Region条件
- 躲过很多次垃圾回收(可以通过
-XX:MaxTenuringThreshold
来设置) - 动态年龄判断(某次GC后存活对象大小超过S区50%,大于超过的对象年龄的对象进入老年代)
1.3.3. 大对象Region
在G1中,大对象的判定规则是对象大小超过Region大小的50%。大对象占用单独的Region,不进入老年代,如果对象大小超过Region,会横跨多个Region来存放(新生代和老年代回收也会判断大对象是否需要回收)。
1.3.4. 新生代+老年代的混合垃圾回收
G1中有一个参数,-XX:InitiatingHeapOccupancyPercent
,默认值是45%。如果老年代占据了堆内存的45%的Region的时候,就会尝试触发 新生代+老年代
一起回收的混合回收阶段。
初始标记
垃圾回收时,第一阶段,初始标记,需要进入Stop the World
状态,仅仅是标记一下 GC Roots 直接能引用的对象(方法区中的类静态变量、局部变量直接引用等),这个过程会很快。
并发标记
第二阶段,并发标记,业务系统会继续运行,同时去追踪初始标记中的对象,追踪引用的存活对象。这个过程很耗时,但是业务系统可以并行运行,所以影响不大。JVM也会对并发标记阶段对象的修改记录起来(eg:对象新建,对象失去引用等)。
最终标记
第三阶段,最终标记,需要进入Stop the World
状态,会根据并发标记阶段记录的对象改变数据,最终标记一下这些对象的存活状态。
混合回收
最后阶段,混合回收,这个阶段会计算每个 Region 中存活对象数量占比 及 执行垃圾回收的性能和效率。然后会停止系统运行,根据设置的预期停顿时间,自己评估选择部分 Region 进行回收。
- 注意:老年代堆内存占比达到45%的时候,触发的是“混合回收”(不仅仅是回收老年代,还会回收新生代及大对象)。
回收相关参数
回收阶段可以停止多次进行回收,通过-XX:G1MixedGCCountTarget
设置最后一个阶段执行几次回收(默认8次),就是先停止工作,执行一次混合回收回收掉一些 Region,然后恢复系统运行,再次停止系统运行,又执行一次混合回收回收掉一些 Region,反复8次。这样可以尽可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下业务系统。
回收时根据参数-XX:G1HeapWastePercent
(默认5%)判断,在混合回收的时候,对 Region 回收是基于复制算法进行的(没有内存碎片),把要回收的 Region 里的存活对象放入其他 Region,然后这个 Region 中的对象全部清理掉,如果空闲Region达到堆内存的5%,停止混合回收,本次混合回收就结束。
回收限制 Region 的参数-XX:G1MixedGCLiveThresholdPercent
(默认85%),指的是存活对象低于85%的 Region,才可以回收。因为一个Region的存活对象多余85%,要把85%的对象都拷贝到其他 Region,成本是很高的。
回收失败时的 Full GC
进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,就是要把各个 Region 的存活对象拷贝到其他 Region 里去。
拷贝过程中发现没有可以承载存活对象的 Region,就会触发一次失败,一旦失败,就会停止系统运行,采用单线程进行标记、清理和压缩整理,空闲出来一批 Region,这个过程是极慢的。
1.4. G1 优缺点
优势:
- 高吞吐量
- 低延迟(可控停顿时间)
- 适合大堆(8GB+)
劣势:
- 内存占用较高(RSet 和 SATB 维护开销)
- 小堆场景不如 Parallel GC 高效
调优建议:
- 避免 Full GC:
- 确保
InitiatingHeapOccupancyPercent
设置合理 - 增加
G1ReservePercent
预留空间
- 确保
- 避免 Full GC:
1.5. 总结
- JDK 9+:
- 默认 G1,平衡吞吐量和延迟。
- 超低延迟 → ZGC(Oracle JDK)或 Shenandoah(OpenJDK)。