1. G1垃圾回收器概述
G1(Garbage-First)是JDK 7中引入的面向服务端应用的垃圾回收器,它的设计目标是替代CMS回收器,成为HotSpot虚拟机默认的垃圾回收器。与传统的分代收集器不同,G1采用了一种全新的内存布局方式——将堆划分为多个大小相等的Region(区域),每个Region都可以根据需要扮演新生代的Eden空间、Survivor空间,或者老年代空间。
重要提示:从JDK 9开始,G1已经成为默认的垃圾回收器,取代了Parallel Scavenge + Parallel Old的组合。
G1的核心思想是"化整为零":不再坚持固定大小和固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region都可以根据需要扮演不同的角色。这种设计使得G1能够避免全堆扫描,只关注包含垃圾最多的区域(这也是"Garbage-First"名称的由来)。
2. G1的核心设计原理
2.1 Region分区模型
G1将堆划分为多个大小相等的Region,默认情况下,堆被划分为约2048个Region,每个Region的大小根据堆内存大小动态决定,计算公式为:
code复制RegionSize = HeapSize / 2048
但Region大小必须在1MB到32MB之间,且必须是2的幂次方。例如:
- 堆大小为4GB时:RegionSize = 4GB/2048 = 2MB
- 堆大小为8GB时:RegionSize = 8GB/2048 = 4MB
这种设计带来了几个关键优势:
- 可以避免传统分代收集器的内存碎片问题
- 允许更灵活的内存分配和回收策略
- 使得大对象(Humongous Object)可以跨多个Region存储
2.2 停顿预测模型
G1通过建立可预测的停顿时间模型来满足用户设定的停顿时间目标(通过-XX:MaxGCPauseMillis参数指定,默认200ms)。G1会跟踪各个Region的回收价值(回收所获得的空间大小及回收所需的时间),在后台维护一个优先列表,每次根据用户设定的停顿时间,优先回收价值最大的Region。
这个预测模型基于以下数据:
- 每个Region的回收历史数据
- 跨代引用关系
- 对象存活率统计
- 卡表(Card Table)和记忆集(Remembered Set)的状态
2.3 记忆集与写屏障
G1使用记忆集(Remembered Set)来避免全堆扫描。每个Region都有一个对应的记忆集,记录从其他Region指向本Region的引用。当进行垃圾回收时,只需要扫描记忆集就可以确定哪些对象是存活的,而不需要扫描整个堆。
写屏障(Write Barrier)用于维护记忆集的正确性。当程序修改对象引用关系时,写屏障代码会被执行,记录跨Region的引用关系。G1使用一种称为"Post-Write Barrier"的技术,其伪代码如下:
code复制void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 写前屏障,用于并发标记
*field = new_value; // 实际写操作
post_write_barrier(field, new_value); // 写后屏障,用于更新记忆集
}
3. G1的回收过程详解
3.1 年轻代GC
G1的年轻代GC与其他收集器类似,采用复制算法,但有以下特点:
- 年轻代Region的总大小会根据-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent动态调整
- 每次年轻代GC都会计算下一次年轻代所需的大小
- 存活对象被复制到Survivor Region或晋升到老年代Region
年轻代GC的步骤:
- 根扫描:扫描GC Roots直接引用的对象
- 更新记忆集:处理dirty card queue中的记录
- 处理记忆集:识别被老年代对象引用的年轻代对象
- 对象复制:将存活对象复制到Survivor Region或老年代
- 引用处理:处理软引用、弱引用等特殊引用
3.2 混合GC
当堆内存使用率达到一定阈值(默认45%,由-XX:InitiatingHeapOccupancyPercent控制)时,G1会启动混合回收(Mixed GC),既回收年轻代Region,也回收部分老年代Region。
混合GC的关键步骤:
- 初始标记(Initial Mark):标记从GC Roots直接可达的对象,需要STW
- 并发标记(Concurrent Mark):从GC Roots开始对堆中对象进行可达性分析
- 最终标记(Final Mark):处理SATB(Snapshot-At-The-Beginning)缓冲区中的记录,需要STW
- 筛选回收(Live Data Counting and Evacuation):根据停顿预测模型选择回收价值最高的Region进行回收
3.3 全堆回收(Full GC)
当G1无法在分配对象时找到足够的可用Region时,会触发Full GC。Full GC是单线程的(Serial GC),会导致长时间的停顿,应该尽量避免。
常见触发Full GC的情况:
- 晋升失败(Promotion Failure):年轻代对象晋升时老年代没有足够空间
- 分配失败(Allocation Failure):无法为对象分配新的Region
- 大对象分配失败(Humongous Allocation Failure)
4. G1的关键参数调优
4.1 基本参数
- -XX:+UseG1GC:启用G1垃圾回收器
- -XX:MaxGCPauseMillis=200:目标最大停顿时间(毫秒)
- -XX:InitiatingHeapOccupancyPercent=45:触发并发标记周期的堆占用率阈值
- -XX:G1HeapRegionSize=n:设置Region大小(1MB-32MB,2的幂次方)
4.2 内存分配参数
- -XX:G1NewSizePercent=5:年轻代最小占比
- -XX:G1MaxNewSizePercent=60:年轻代最大占比
- -XX:GCTimeRatio=9:GC时间与应用时间的目标比率
- -XX:ConcGCThreads=n:并发标记阶段的线程数
4.3 高级调优参数
- -XX:G1ReservePercent=10:保留内存百分比,用于晋升失败时的回退
- -XX:G1HeapWastePercent=5:允许的堆浪费百分比
- -XX:G1MixedGCLiveThresholdPercent=85:老年代Region存活对象占比阈值
- -XX:G1MixedGCCountTarget=8:混合GC的预期次数
5. G1的实践技巧与问题排查
5.1 最佳实践
- 合理设置MaxGCPauseMillis:不要设置过小(如<50ms),否则会导致频繁GC
- 监控IHOP值:如果并发标记经常启动太晚,可以适当降低InitiatingHeapOccupancyPercent
- 避免大对象:Humongous对象会带来额外的开销
- 合理设置堆大小:G1需要一定的堆空间来发挥优势
5.2 常见问题排查
问题1:频繁Full GC
- 可能原因:内存不足、IHOP设置过高、晋升失败
- 解决方案:增加堆大小、降低IHOP、调整G1ReservePercent
问题2:长时间并发标记
- 可能原因:堆过大、ConcGCThreads设置过小
- 解决方案:增加并发标记线程数、考虑分拆服务
问题3:GC停顿时间波动大
- 可能原因:堆内存碎片化、Region大小不合适
- 解决方案:尝试固定RegionSize、检查大对象分配
5.3 监控与日志分析
启用详细GC日志:
code复制-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:gc.log
关键指标监控:
- GC停顿时间分布
- 每次GC回收的内存大小
- 年轻代/老年代比例变化
- 并发标记周期频率
使用工具分析:
- GCViewer:可视化分析GC日志
- JStat:实时监控GC状态
- JVisualVM:全面的JVM监控
6. G1与其他收集器的对比
6.1 G1 vs CMS
-
内存模型:
- CMS:传统分代模型(连续的新生代和老年代)
- G1:Region分区模型
-
内存碎片:
- CMS:使用标记-清除算法,会产生碎片
- G1:整体上看是标记-整理,局部是复制算法,减少碎片
-
停顿时间预测:
- CMS:无法预测停顿时间
- G1:可配置目标停顿时间
-
大堆表现:
- CMS:在大堆(>6GB)时表现下降
- G1:设计目标就是大堆应用
6.2 G1 vs ZGC/Shenandoah
-
停顿时间:
- G1:追求10ms-几百ms的停顿
- ZGC/Shenandoah:目标<10ms的停顿
-
内存开销:
- G1:记忆集和写屏障开销较大
- ZGC/Shenandoah:使用颜色指针等技术,减少内存开销
-
适用场景:
- G1:通用场景,JDK8-11的主流选择
- ZGC/Shenandoah:超低延迟要求的场景
在实际生产环境中,G1的调优需要结合具体应用特点和性能指标。从我处理过的多个高并发系统案例来看,对于堆内存4GB-16GB的应用,G1通常能提供最佳的综合性能。一个常见的误区是过度追求低停顿时间而将MaxGCPauseMillis设得过小,这反而会导致更高的GC频率和总体开销。建议先使用默认参数,再根据监控数据逐步调整。