G1 垃圾收集器概述
概述
G1(Garbage-First)垃圾收集器是专门为配备多颗处理器及大容量内存的机器设计的服务端垃圾收集器。其主要目标是在尽量满足GC停顿时间要求的同时,也具备高吞吐量能力。
与传统基于分代收集理论的垃圾收集器(如Serial、Parallel、CMS等)不同,G1和ZGC逐渐淡化了物理分代的概念。G1保留了年轻代和老年代的概念,但不再是物理隔离,而是逻辑上的概念,实现了更灵活的内存管理。
适用场景
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间特别长
- 停顿时间要求在500ms以内
G1 的核心和内存结构
分区(Region)机制
G1将Java堆内存划分为多个大小相等的独立区域(Region),每个Region都有一个连续的虚拟内存范围。JVM最多可以有2048个Region,每个Region的大小可以通过以下公式计算:
Region大小 = 堆内存 / 2048
- 例如,4096MB的堆内存,每个Region的大小为4096/2048 = 2MB。
- 可以通过参数
-XX:G1HeapRegionSize
调整每个Region的大小,但一般不建议修改。
分代概念
G1在淡化分代理论上不彻底,保留了年轻代和老年代的概念,但它们不再是物理隔离,而是Region的集合:
- 年轻代默认占堆内存的5%,可通过
-XX:G1NewSizePercent
设置初始占比 - 年轻代占比最多不超过60%,可通过
-XX:G1MaxNewSizePercent
调整 - 年轻代中的Eden和Survivor区域默认比例为8:1:1
- 一个Region可能之前是年轻代,垃圾回收后可能变成老年代,Region功能动态变化
巨型对象处理
G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代。大对象的判定规则是:一个对象超过了一个Region大小的50%。例如,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous区。太大的对象可能会横跨多个Region存放。
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代空间,避免因为老年代空间不够的GC开销。Full GC时会一并回收Humongous区。
关键数据结构
收集集合(CSet)
一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区。CSet中的分区可以来自Eden空间、Survivor空间或老年代。CSet会占用不到整个堆空间的1%大小。
已记忆集合(RSet)
RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。
RSet其实是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。
Snapshot-At-The-Beginning(SATB)
SATB是维持并发GC正确性的一个手段,G1GC的并发理论基础就是SATB。SATB算法创建了一个对象图,它是堆的一个逻辑"快照"。标记数据结构包括了两个位图:previous位图和next位图。
SATB是一个快照标记算法,在并发标记过程中,G1 GC使用了SATB write barrier来解决对象引用变更导致快照不完整的问题。
G1 的垃圾收集过程
垃圾收集的四个操作
G1收集器的收集活动主要有四种操作:
- 新生代垃圾收集
- 后台收集、并发周期
- 混合式垃圾收集
- 必要时的Full GC
新生代垃圾收集(Young GC)
Young GC并不是在Eden区放满后立即触发。G1会计算现在Eden区回收大概需要多长时间,如果回收时间远小于-XX:MaxGCPauseMills
设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC。直到下一次Eden区放满,G1计算回收时间接近参数设定的值,才会触发Young GC。
Young GC的过程:
- 暂停所有其他线程(STW)
- 记录gc roots直接能引用的对象
- 多线程并行执行垃圾收集
- 存活对象被拷贝到新的Survivor分区或老年代
并发标记周期
当老年代的堆占有率达到参数-XX:InitiatingHeapOccupancyPercent
设定的值(默认45%)时触发并发标记周期。这个过程包括多个阶段:
初始标记(Initial Mark,STW)
- 与Young GC一起进行,暂停所有应用线程。快速地从GC Roots开始,标记出直接可达的存活对象,并扫描Survivor区(作为根的一部分),为并发标记阶段做准备
- 设置TAMS:在初始标记阶段,G1会为每个Region设置两个关键的指针:Next TAMS 和 Previous TAMS。
- TAMS (Top at Mark Start) 的作用是划分并发标记期间新分配的对象。
- 在TAMS指针之上新分配的对象,在本次标记周期中会被隐式地视为存活对象,无需额外标记。
- 这个设计巧妙地将并发标记过程中应用程序不断产生的新对象纳入管理,保证了标记的正确性。
整个过程为 :
STW -> 暂停应用线程 -> 进行Young GC -> 同时完成GC Roots直接引用标记和Survivor区扫描 -> 设置每个Region的TAMS -> 恢复应用线程
并发标记(Concurrent Marking)
- 根据初始标记阶段的结果,与应用线程并发地遍历整个对象图,标记出所有的存活对象
- 多线程并发执行,与应用线程同时运行
- 利用trace算法找到所有存活对象,记录在bitmap中
- 只遍历TAMS之下的对象
最终标记(Remark,STW)
- 暂停整个应用
- 处理SATB日志缓冲区和所有更新的引用
- 找出所有未被标记的存活对象
- 处理引用(soft/weak/final/phantom/JNI)
清理(Cleanup,STW)
- 首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划 。
- 举个例子 老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200ms,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。
- 这个阶段 G1因为内部实现太复杂暂时没实现并发回收。到了ZGC,Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本。
- 不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中, G1采用复制算法回收几乎不会有太多内存碎片。
混合垃圾收集(Mixed GC)
并发标记完成后,会进行一次Mixed GC。Mixed GC与Young GC类似,但有两点区别:
- 日志中有"mixed"标记
- CSet包含通过并发标记确定的老年区域
Mixed GC回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区。
Full GC
当Mixed GC过程中发现没有足够的空region能够承载拷贝对象时,会触发Full GC。Full GC会停止系统程序,采用单线程进行标记、清理和压缩整理,空闲出一批Region供下一次MixedGC使用。这个过程非常耗时,应尽量避免。
G1 的GC 日志
4.1 日志打印参数配置
-Xloggc:<path> # gc log 的输出路径
-XX:+PrintGC # 输出 gc log
-XX:+PrintGCDetails # 输出 gc 详细信息
-XX:+PrintGCDateStamps # 记录 gc 启动时的系统时间
-XX:+PrintGCTimeStamps # 记录 gc 启动时相对于 jvm 启动的相对时间
-XX:+PrintAdaptiveSizePolicy # 打印自适应大小策略信息
-XX:+PrintTenuringDistribution # 打印 Survivor 对象年龄分布
-XX:+PrintReferenceGC # 打印各种引用的处理时间
日志自动分割
按重启时间自动分割
-Xloggc:/data/var/gclog/gc-%t.log
Young GC 日志解析
Young GC日志包含六部分内容:
- 基本信息:日期时间、JVM启动间隔、收集类型、耗时
- 并行任务信息(STW):并行收集任务执行详情
- 串行任务信息:Code Root Fixup、Code Root Purge等
- 其他串行操作信息:Choose CSet、Ref Proc等
- 各代空间变化统计:Eden、Survivors、Heap变化
- 回收花费时间统计:user、sys、real时间
并发标记周期日志解析
并发标记周期分为六个步骤,每个步骤在日志中都有相应标记:
- 初始标记:
GC pause (G1 Evacuation Pause) (young) (initial-mark)
- 根分区扫描:
GC concurrent-root-region-scan-start/end
- 并发标记:
GC concurrent-mark-start/end
- 最终标记:
GC remark [Finalize Marking / GC ref-proc / Unloading]
- 清除阶段:
GC cleanup
- 并发清除:
GC concurrent-cleanup-start/end
Mixed GC 日志解析
Mixed GC日志与Young GC基本相同,只有两点区别:
- 有mixed标记:
GC pause (G1 Evacuation Pause) (mixed)
- CSet包含通过并发标记确定的老年区域
Full GC 日志解析
Full GC日志包含三部分信息:
- 触发原因
- 发生频率
- full gc的耗时
应尽量避免Full GC,它是整个堆内存的完整收集,通常意味着漫长的STW暂停。
G1调优
启用与基本设置
# 启用G1垃圾收集器
-XX:+UseG1GC
# 设置最大堆内存
-Xmx4g
# 设置初始堆内存
-Xms4g
将-Xmx和-Xms设置为相同值,避免堆内存动态调整带来的性能开销。
暂停时间目标
# 设置期望的最大GC暂停时间(毫秒)
-XX:MaxGCPauseMillis=200
调优方案:
- 默认值为200ms,通常设置在100-250ms之间
- 设置过低会导致GC过于频繁,影响吞吐量
- 设置过高可能导致单次暂停时间过长
- 应根据实际应用响应时间要求进行调整
内存区域配置
Region大小设置
# 设置Region大小(1MB-32MB,必须是2的幂)
-XX:G1HeapRegionSize=4m
通常选择方案:
- 堆内存 < 4GB: Region大小设置为1-2MB
- 堆内存 4-8GB: Region大小设置为2-4MB
- 堆内存 8-16GB: Region大小设置为4-8MB
- 堆内存 > 16GB: Region大小设置为8-16MB
年轻代配置
# 新生代初始占比(默认整堆5%)
-XX:G1NewSizePercent=5
# 新生代最大占比(默认60%)
-XX:G1MaxNewSizePercent=60
# Survivor区填充比例(默认50%)
-XX:TargetSurvivorRatio=50
# 对象晋升老年代年龄阈值(默认15)
-XX:MaxTenuringThreshold=15
调优方案:
- 增大G1MaxNewSizePercent可减少Young GC频率但增加单次暂停时间
- 降低TargetSurvivorRatio可使对象更早晋升到老年代
- 根据对象存活时间调整MaxTenuringThreshold
并发调优参数
并发标记触发阈值
# 老年代占用整堆比例阈值,触发并发标记(默认45%)
-XX:InitiatingHeapOccupancyPercent=45
调优方案:
- 对于分配率高的应用,可适当降低此值(如35-40%)
- 对于内存充足的应用,可适当提高此值(如50%)
- 观察GC日志,确保在并发周期完成前不会发生Full GC
GC线程数配置
# 并行GC线程数(通常等于CPU核心数)
-XX:ParallelGCThreads=8
# 并发GC线程数(通常为ParallelGCThreads的1/4)
-XX:ConcGCThreads=2
配置方案:
- ParallelGCThreads = CPU线程数
- ConcGCThreads = max(1, ParallelGCThreads / 4)
- 在CPU密集型应用中,可适当减少GC线程数
混合收集调优
混合收集控制
# Region中存活对象比例阈值,低于此值才可能被回收(默认85%)
-XX:G1MixedGCLiveThresholdPercent=85
# 一次混合回收周期中的收集次数(默认8次)
-XX:G1MixedGCCountTarget=8
# 本次回收后空出Region占比阈值(默认5%)
-XX:G1HeapWastePercent=5
调优方案:
- 降低G1MixedGCLiveThresholdPercent可使更多Region被回收,但可能增加暂停时间
- 增加G1MixedGCCountTarget可减少单次回收Region数量,降低单次暂停时间
- G1HeapWastePercent设置回收过程中空闲Region比例达到多少时停止混合回收
巨型对象处理
# 跟踪并输出超大对象回收相关信息
-XX:+G1TraceEagerReclaimHumongousObjects
# 立即回收不可达的巨型对象
-XX:G1EagerReclaimHumongousObjects=true
调优方案:对于频繁分配大对象的应用,启用巨型对象跟踪可帮助识别内存问题。
高级诊断参数
详细日志记录
# 解锁诊断参数
-XX:+UnlockDiagnosticVMOptions
# 打印GC详细时间信息
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCApplicationConcurrentTime
# 打印Region信息
-XX:+G1PrintHeapRegions
# 打印RSet详细信息
-XX:+G1SummarizeRSetStats
性能监控参数
# 打印自适应大小策略信息
-XX:+PrintAdaptiveSizePolicy
# 打印晋升年龄分布
-XX:+PrintTenuringDistribution
# 打印引用处理时间
-XX:+PrintReferenceGC
高级功能
基础调优流程
设定暂停时间目标
-XX:MaxGCPauseMillis=200
设置堆大小
-Xms4g -Xmx4g
配置Region大小
-XX:G1HeapRegionSize=4m
调整IHOP阈值
-XX:InitiatingHeapOccupancyPercent=40
根据应用特征调优
高分配率应用:
-XX:G1NewSizePercent=10
-XX:G1MaxNewSizePercent=70
-XX:InitiatingHeapOccupancyPercent=35
大内存应用:
-XX:G1HeapRegionSize=8m
-XX:G1MixedGCLiveThresholdPercent=90
-XX:G1HeapWastePercent=10
低延迟应用:
-XX:MaxGCPauseMillis=100
-XX:G1MixedGCCountTarget=16
-XX:ConcGCThreads=4
出现Full GC怎么办
GC日志中出现Full GC
解决方案:
- 降低IHOP阈值,更早启动并发标记
-XX:InitiatingHeapOccupancyPercent=35
- 增加并发标记线程数
-XX:ConcGCThreads=4
- 调整混合回收参数
-XX:G1MixedGCLiveThresholdPercent=80 -XX:G1HeapWastePercent=10
并发模式失败怎么办
并发标记周期被中止
解决方案:
- 确保堆内存足够
- 增加并发线程数
- 优化应用代码,降低对象分配率
GC监控
关键监控指标
- GC暂停时间:应接近MaxGCPauseMillis设置
- 吞吐量:GC时间占比应低于10%
- 内存使用:老年代使用率应稳定在IHOP阈值以下
- 晋升速率:对象从年轻代晋升到老年代的速率
调优标准
- GC暂停时间满足应用需求
- 应用吞吐量达到预期
- 没有Full GC发生
- 堆内存使用率合理
- GC开销可控(通常<10%)
配置示例
Web服务器配置示例
# 基础配置
-Xms8g -Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
# 区域配置
-XX:G1HeapRegionSize=4m
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=60
# 并发配置
-XX:InitiatingHeapOccupancyPercent=45
-XX:ConcGCThreads=3
# 混合回收配置
-XX:G1MixedGCLiveThresholdPercent=85
-XX:G1MixedGCCountTarget=8
-XX:G1HeapWastePercent=5
# 监控配置
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
大数据处理应用配置
# 大内存配置
-Xmx16g -Xms16g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=300
# 大Region配置
-XX:G1HeapRegionSize=8m
# 高分配率优化
-XX:G1MaxNewSizePercent=70
-XX:InitiatingHeapOccupancyPercent=35
# 并发能力提升
-XX:ConcGCThreads=4
-XX:ParallelGCThreads=8
# 混合回收优化
-XX:G1MixedGCLiveThresholdPercent=80
-XX:G1MixedGCCountTarget=12