G1垃圾收集器

G1垃圾收集

G1收集器介绍 - deyang - 博客园

G1了解

G1为什么是“逻辑分代,物理分区”?

这是一种完美的折衷,让G1同时获得了:

优势来源 带来的好处
逻辑分代 1. 遵循分代理论:享受年轻代高频收集、高死亡率带来的效率红利。
2. 记忆集优化:减少跨代引用扫描的开销。
3. 对象生命周期管理:合理的对象晋升路径。
物理分区 1. 可预测的停顿:通过选择回收集合,精确控制每次GC的耗时。
2. 增量式整理:解决内存碎片问题,避免Full GC。
3. 极大的灵活性:内存管理不再受固定大小、连续空间的限制。

G1是一个自适应收集器,它会在每次垃圾收集(包括年轻代收集Young GC和混合收集Mixed GC)之后,动态地调整年轻代的大小(即Region的数量)。

  • 因为会动态地调整年轻代的大小,因此,当年轻代扩大时,老年代就缩小;当年轻代缩小时,老年代就扩大。 老年代的大小是年轻代调整后的一个副产品

G1的三种主要回收类型

1. 年轻代垃圾收集

  • 触发条件:当Eden区的Region被填满时触发。
  • 回收目标只回收年轻代的Region(包括Eden和Survivor区)。
  • 工作方式:这是一个STW的、并行拷贝的收集过程。存活的对象被拷贝到新的Survivor区或晋升到老年代的Region。
  • 特点:和传统的Young GC非常相似,但回收的是一组年轻代Region,而不是一个连续的年轻代空间。
    • G1的年轻代收集实现了与ParNew相似的收集效果,但借助更先进的底层架构(Region),实现了更高的扫描效率和更大的灵活性。

2. 混合垃圾收集

这是G1最核心、最具特色的回收方式。

  • 触发条件:在并发标记周期完成之后触发的一系列收集。
  • 回收目标同时回收年轻代Region和一部分被标记为包含最多垃圾的老年代Region
  • 工作方式
    1. 首先,G1会像Young GC一样回收所有年轻代Region。
    2. 同时,它会根据“Garbage-First”的原则,从老年代Region中选择垃圾最多(即存活对象最少)的几个Region加入到回收集合中一并回收。
    3. 这个过程会进行多次,直到回收了足够多的老年代垃圾(达到 G1HeapWastePercent 阈值为止)。
  • 特点:这是G1实现其核心目标(可控停顿)的关键。它增量地、部分地回收老年代,避免了像CMS那样一次性处理整个老年代带来的长停顿风险。

3. Full GC

  • 触发条件:这是G1的失败保障机制,在极端情况下触发,例如:
    • 并发标记周期还未完成,老年代就被填满了(晋升失败)。
    • 在拷贝存活对象时,找不到空的Region来存放( evacuation failure)。
  • 回收目标:回收整个堆(包括年轻代和老年代的所有Region)。
  • 工作方式:这是一个STW的、单线程的(Serial GC)标记-整理过程,停顿时间非常长,是应该极力避免的情况。
  • 特点:如果你的应用触发了G1的Full GC,说明调优有问题(比如IHOP设置不当、堆大小不足等)。

总结

收集类型 触发条件 使用的算法 回收范围 频率 目标
年轻代收集 Eden区满 标记-复制 仅年轻代 Region 快速回收新对象
混合收集 并发标记周期完成后 标记-复制 年轻代 + 部分老年代 Region 中等 核心! 增量回收老年代,控制停顿
Full GC 内存分配失败/晋升失败 标记-整理 整个堆 的所有Region 低(应避免) 救命机制,代价高昂

大部分都是年轻代收集混合收集,都是复制算法,那为什么说G1是标记-整理算法实现的???

核心答案:G1的整体回收器类型被归类为“标记-整理”,不是根据它每次回收时使用的 微观 算法,而是根据它在 宏观 上管理整个堆内存、对抗碎片化的整体策略。

可以这样理解:

  • 复制算法描述的是 “如何从一个Region里取出存活对象”
  • 标记-整理描述的是 “如何通过移动对象来消除整个堆的碎片”

G1通过无数次小规模的、基于复制的回收,最终达成了整理整个堆内存的宏观效果。这正是它被称为“标记-整理”收集器的原因。


一个完美的比喻

想象一下整理一个杂乱无章的仓库:

  • 传统标记-整理(Serial Old):就像闭馆一天,把所有货物(存活对象)从货架上搬下来,然后从仓库的一端开始,紧密地、有序地重新摆放回去。停顿时间长,但整理彻底。
  • G1的标记-整理:就像仓库照常营业,但组织了一个小分队。这个小分队每天只整理几个最乱的货架(回收集合)。他们把上面有用的货物(存活对象)拣出来,搬到一些空的货架上紧凑地放好(复制到空闲Region),然后就把原来的货架清空备用。日积月累,整个仓库也变得整齐有序,而且营业几乎不中断。

小分队在整理单个货架时用的“拣货-搬货”方法,就是“复制算法”。
而通过这种持续不断“拣货-搬货”来维持整个仓库整洁的长期策略,就是“标记-整理”。

个人总结

G1:总结来说,年轻代收集是ParNew相似的收集效果,Full GC是Serial Old收集器相似的收集效果,混合收集才是核心,它有初始标记、并发标记、最终标记、筛选回收等阶段。

  1. 年轻代收集:类似 ParNew 的效果
    • 核心:并行、STW的标记-复制算法。
    • 目标:高效回收年轻代,高吞吐。
    • 定位:常规的、高频发生的收集行为。
  2. 混合收集:G1 的核心与精髓
    • 核心:这是一个完整的并发标记周期的成果,包含您提到的几个关键阶段:
      • 初始标记:短暂的STW,标记GC Roots直接关联的对象。
      • 并发标记:GC与用户线程并发执行,标记全堆存活对象。
      • 最终标记:STW,处理并发阶段产生的引用变化。
      • 筛选回收/清理:STW,统计垃圾并选择收益最高的老年代Region,准备混合收集。
    • 目标增量地、部分地回收老年代,实现可控的停顿时间内存整理
    • 定位:G1实现其设计目标(低延迟、无碎片)的关键。
  3. Full GC:类似 Serial Old 的效果
    • 核心:单线程、STW的标记-整理算法。
    • 目标:在内存分配失败的紧急情况下进行保底回收,避免OOM。
    • 定位失败预案,应极力避免发生。

G1对比CMS

CMS:总结来说,年轻代收集配合ParNew的收集。CMS进行老年代收集,它有初始标记、并发标记、最终标记、筛选回收等阶段。无法忍受内存碎片时,进行Full GC,使用Serial Old收集器。

CMS vs G1:核心逻辑对比

方面 CMS G1
年轻代收集 配合ParNew(并行复制) 内置的年轻代收集(并行复制)
老年代正常收集 并发标记周期 (标记-清除) 混合收集 (标记-复制)
老年代收集算法 标记-清除 标记-复制
是否解决碎片 ,这是其最大痛点 ,通过复制算法增量整理
失败后的Full GC Serial Old(标记-整理) Serial Old(标记-整理)

CMS vs. G1:收集事件交织对比

特性 CMS (ParNew + CMS) G1
核心设计 两套独立的收集系统(ParNew管年轻代,CMS管老年代) 一套统一的收集系统
年轻代与老年代收集的关系 独立且可能交织 互斥且不会交织
并发周期与Young GC 可以交织。CMS的并发标记阶段(非STW)可以与Young GC的STW同时发生。Young GC会打断并发标记,增加其复杂度。 可以重叠。G1的并发标记阶段(非STW)可以与Young GC的STW同时发生。Young GC会记录引用变化,供后续最终标记阶段处理。
STW阶段是否会重叠 不会。例如,Young GC的STW和CMS重新标记的STW不会同时发生,会相互等待。 不存在此问题。因为一次GC事件只能是Young GC或Mixed GC,二者选一。
回收集合 分离的: - Young GC: 年轻代 - CMS GC: 老年代 统一的、包含关系的: - Young GC: 仅年轻代Region - Mixed GC: 年轻代Region + 部分老年代Region
形象比喻 两个不同的施工队: - 一队专门修小路(Young GC) - 一队专门修主路(CMS GC) 两队可能同时工作,需要协调,但各自的工具和区域是分开的。 一个全能施工队: - 平时只修小路(Young GC) - 到了特定时期,就执行“小路+部分主路”的联合维修计划(Mixed GC) 一次只执行一个计划。

CMS 与 G1 收集时机对比总览

收集类型 CMS (ParNew + CMS) G1
年轻代收集 触发时机Eden区满
执行者:ParNew收集器
特点:STW、并行、复制算法。是独立于老年代收集的事件。
触发时机Eden区的Region被填满
执行者:G1内置的年轻代收集器
特点:STW、并行、复制算法。是独立于混合收集的事件。
老年代/混合收集 触发时机老年代空间使用率达到 CMSInitiatingOccupancyFraction 阈值(如68%)。
执行者:CMS收集器
特点并发标记-清除。包含初始标记、并发标记、重新标记、并发清除。旨在避免Full GC。
触发时机整个堆的使用率达到 InitiatingHeapOccupancyPercent 阈值(如45%),且并发标记周期完成后
执行者:G1收集器
特点并发标记-复制。并发标记完成后,启动一系列混合收集,每次回收年轻代 + 部分老年代。旨在增量整理并避免Full GC。
Full GC 触发时机
1. 并发模式失败(并发收集赶不上分配速度)
2. 晋升失败(老年代碎片导致无连续空间)
3. 元空间耗尽等。
执行者:Serial Old收集器
特点:STW、单线程、标记-整理算法。是失败预案,代价极高。
触发时机
1. 晋升失败(拷贝存活对象时无空闲Region)
2. 并发模式失败(并发标记期间堆被填满)
3. 巨型对象分配失败等。
执行者:Serial Old收集器
特点:STW、单线程、标记-整理算法。是失败预案,代价极高。

GC之间正确的时序与逻辑关系

场景一:正常的Young GC(大部分情况)

  1. 触发:Eden区Region被填满。
  2. 行动:G1启动一次 Young GC
  3. 回收范围仅限年轻代Region(Eden和Survivor)。
  4. 结果:存活对象晋升到Survivor区或老年代Region。Young GC结束。应用线程继续运行。
  5. 此时,并发标记周期并未启动。这只是一次非常普通的、频繁发生的Young GC。

场景二:并发标记周期的触发(独立事件)

  1. 触发条件:这是一个独立的条件,与Young GC无直接关系。当整个堆的使用率达到 -XX:InitiatingHeapOccupancyPercent (IHOP) 设定的阈值时(例如,默认45%),G1会在后台启动并发标记周期
  2. 触发时机:这个检查可能在一次Young GC之后之前期间发生。只要堆使用率达标,它就会触发。
  3. 行动:并发标记周期是一个漫长的、多阶段的后台任务,包括:
    • 初始标记(STW,很短)
    • 根区域扫描
    • 并发标记
    • 最终标记(STW)
    • 清理(STW)

场景三:混合收集的触发(并发标记周期的结果)

  1. 触发条件当且仅当并发标记周期完全结束后,G1才获得了足够的信息(知道了每个老年代Region的垃圾含量)。
  2. 行动:G1开始启动一系列 混合垃圾收集
  3. 回收范围:每次Mixed GC都会:
    • 首先,回收所有的年轻代Region(就像一次Young GC)。
    • 同时,选择一部分垃圾最多(存活对象最少)的老年代Region加入回收集合进行回收。

Young GC到Mixed GC的流程

让我们用一段“故事”来描绘的过程:

  1. 第1-100次GC:应用不断运行,触发了很多次 Young GC。老年代逐渐被填满。
  2. 某个时刻jstat 显示堆使用率达到了45%(IHOP阈值)。
  3. 触发事件:G1在后台启动了并发标记周期。这个周期可能持续几次Young GC的时间。
  4. 并发标记进行中:Young GC依然在因为Eden满而独立地、照常发生
  5. 并发标记结束:标记周期完成了。现在G1有一个“垃圾密度”排行榜。
  6. 第101次及后续几次GC:从现在开始,接下来触发的几次GC不再是单纯的Young GC,而是 Mixed GC。每次Mixed GC都会在回收年轻代的同时,带走一些“垃圾排行榜”上名列前茅的老年代Region。
  7. Mixed GC系列结束:当回收的老年代空间足够多(达到 G1HeapWastePercent 阈值),Mixed GC系列停止。
  8. 回到常态:之后的GC又变回了普通的 Young GC,直到堆使用率再次达到IHOP,开启下一个并发标记周期。

总结

  • Young GC并发标记周期 的触发是两个独立的循环
    • Young GC循环:Eden满 -> Young GC -> Eden满 -> Young GC ...
    • 并发标记循环:堆使用率 >= IHOP -> 启动并发标记 -> 标记完成 -> 启动Mixed GC系列 -> 回到起点
  • 混合收集是并发标记周期的“果实”,而不是某次Young GC的“升级”。
  • 一次GC是Young GC还是Mixed GC,取决于当时并发标记周期是否已完成并且Mixed GC系列是否正在进行中

G1使用调参

具体怎么调,问deepseek DeepSeek | 深度求索

-XX:MaxGCPauseMillis=200    # 根据你的延迟要求调整
-XX:G1NewSizePercent=30     # 年轻代最小比例
-XX:G1MaxNewSizePercent=40  # 年轻代最大比例  
-XX:InitiatingHeapOccupancyPercent=45  # 并发周期触发阈值

# 实际应用
-Xmx13G -Xms13G 
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1NewSizePercent=15
-XX:InitiatingHeapOccupancyPercent=60
# 基础配置
java -server
     # 堆内存:建议 Xmx 和 Xms 设置相同,避免运行时动态调整
     -Xms8g -Xmx8g
     # 启用 G1
     -XX:+UseG1GC
     # 核心目标:最大暂停时间设为 100ms,可根据实际情况调整
     -XX:MaxGCPauseMillis=100
     # 年轻代大小:限制在堆的 20%~40% 之间,平衡吞吐量和暂停时间
     -XX:G1NewSizePercent=30
     -XX:G1MaxNewSizePercent=40
     # 并发周期触发阈值:稍提高以避免过早启动并发标记
     -XX:InitiatingHeapOccupancyPercent=50
     # 并行线程数:如果是在高配服务器上(如32核),可以显式设置以提高效率
     -XX:ParallelGCThreads=16
     # 并发线程数:防止并发标记跟不上,设置为并行线程的1/3到1/2
     -XX:ConcGCThreads=6
     # 元空间大小:游戏服务器可能加载大量类,需要关注元空间
     -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
     # 重要:开启 GC 日志,这是调优的基石
     -Xloggc:/path/to/your/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
     # 可选:打印晋升详情,辅助分析
     -XX:+PrintTenuringDistribution
     -jar your-game-server.jar

关键调优步骤

  1. 设定基线:使用上述推荐配置启动服务器。
  2. 收集数据:在模拟真实负载或压力测试下运行一段时间,收集 GC 日志。
  3. 分析日志:使用 GCeasyG1GC Log AnalyzerJVM 内置工具 分析日志。
    • 关注 MaxGCPauseMillis 的实际达成情况。
    • 关注是否有 "Evacuation Failure""Full GC"
    • 关注并发标记周期是否正常。
  4. 迭代调优
    • 如果 年轻代 GC 太频繁:尝试增加 G1NewSizePercent
    • 如果 单次 GC 暂停太长:尝试降低 G1MaxNewSizePercent
    • 如果发生 "Evacuation Failure":尝试降低 IHOP 或增加 ConcGCThreads
    • 如果 吞吐量不足:可以适当放宽 MaxGCPauseMillis

记住,没有放之四海而皆准的最优配置,最佳参数需要通过监控和分析你的特定应用负载来获得。

G1实例

核心思想:将堆划分为多个大小相等的 Region

  • G1将整个Java堆(包括年轻代和老年代)划分为多个大小相等的独立内存区域,称为 Region
  • 每个Region在某一时刻,有且只有一个角色:EdenSurvivorOld Region。Region的角色是可以变化的,一个Region现在可能是Eden,下次GC后可能变成Survivor,再下次可能变成Old。
  • Region的大小可以通过 -XX:G1HeapRegionSize 参数指定,范围从1M到32M,且必须是2的幂。JVM会根据堆的初始和最大尺寸自动决定一个合理的Region大小。
  • 年轻代 (Young Generation):由一组扮演 Eden 角色的Region组成。
  • 老年代 (Tenured/Old Generation):由一组扮演 Old 角色的Region组成。

初始假设

  • 堆大小20个Region (R1-R20),每个1M。
  • GC相关参数
    • 对象晋升老年代的年龄阈值(-XX:MaxTenuringThreshold)为 15,但JVM会根据运行时情况动态调整。
    • 触发Mixed GC的堆占用率阈值(-XX:InitiatingHeapOccupancyPercent)为 45%

阶段一:第一次Young GC前(初始状态)

  • JVM开始分配对象。它从空闲的Region中划出一部分作为初始的Eden区
  • 假设它划出了 R1, R2, R3, R4 这4个Region作为Eden。
  • 此时,没有任何Survivor Region,因为还没有发生过GC,没有存活对象需要晋升。
Region 编号 角色 状态说明
R1 - R4 Eden 正在被新创建的对象填充。
R5 - R20 Old (空闲) 全部未被使用,属于空闲列表。

阶段二:第一次Young GC

触发条件:R1-R4(Eden区)被新对象完全填满。

GC过程

  1. STW停顿开始。
  2. 回收目标:只有 R1-R4 (所有Eden Region)。因为没有Survivor,所以不涉及Survivor的回收。
  3. 标记:从GC Roots开始,标记出Eden中所有存活的对象。
  4. 复制存活对象
    • 绝大多数新创建的对象都是“朝生夕死”的,会被直接回收。
    • 极少数存活的对象需要被移动。由于这是第一次GC,JVM会从空闲列表(Old Region)中开辟一块或两块Region作为“To Survivor”。假设它开辟了 R5R6
    • 所有存活对象被复制到新的Survivor区(R5, R6)。此时它们的年龄被设置为1。
    • (在这个阶段,几乎不可能有对象达到晋升老年代的年龄,所以没有晋升发生。)

第一次Young GC后结果

Region 编号 GC前角色 GC后角色 变化说明
R1 - R4 Eden Old (空闲) 被完全清空,所有对象被回收。Region归还给空闲列表。
R5, R6 Old (空闲) Survivor (To) 被首次征用,成为Survivor区,存放年龄为1的首次存活对象。
R7 - R20 Old (空闲) Old (空闲) 保持不变。

阶段三:第二次Young GC之前

  • 系统继续运行。JVM立即从空闲列表(R1-R4, R7-R20)中分配出新的Eden Region,比如 R7, R8, R9, R10
  • 新对象被分配到这些新的Eden Region中。
  • R5和R6仍然是Survivor区,里面是年龄为1的存活对象。

第二次Young GC前的状态

Region 编号 角色 状态说明
R7 - R10 Eden 已分配满新对象。
R5, R6 Survivor (From) 有第一次GC后存活的对象(年龄=1)。
R1-R4, R11-R20 Old (空闲) 未被使用。

阶段四:第二次Young GC

触发条件:Eden (R7-R10) 满。

GC过程

  1. 回收目标:R7-R10 (Eden) 和 R5, R6 (From Survivor)。
  2. 标记:标记这些Region中的所有存活对象。
  3. 复制存活对象
    • JVM从空闲列表(比如R11, R12)开辟新的 To Survivor Region。
    • Eden和From Survivor中的存活对象被复制到新的To Survivor。
      • 来自Eden的存活对象,年龄变为1。
      • 来自From Survivor的存活对象,年龄增加1,变为2。
    • 如果某些对象的年龄达到了晋升阈值(例如15),或者To Survivor空间不足,它们会被直接晋升到Old Region(比如R13)。

第二次Young GC后结果

Region 编号 GC前角色 GC后角色 变化说明
R7 - R10 Eden Old (空闲) 被清空,归还空闲列表。
R5, R6 Survivor (From) Old (空闲) 被清空,归还空闲列表。
R11, R12 Old (空闲) Survivor (To) 成为新的Survivor区。
R13 Old (空闲) Old (已用) 存放本次GC中晋升的老年代对象。
R1-R4, R14-R20 Old (空闲) Old (空闲) 保持不变。

触发:Mixed GC

触发条件

  1. 系统继续运行。JVM会立即从空闲的Region中划出新的Eden Region供新对象分配。
  2. 很快,新的Eden Region又被填满,可能又经历了几次Young GC。每次Young GC都会导致Survivor区和老年代的使用率缓慢上升。
  3. 整个堆的使用率超过45%(即 20 * 45% = 9个Region)时,G1会启动Mixed GC

假设在触发Mixed GC时,堆状态如下

  • Eden: R1, R2, R3, R4 (4个Region,已满)
  • Survivor: R5, R6 (2个Region)
  • Old (已用): R7, R8, R9, R10 (4个Region,其中可能有垃圾)
  • Old (空闲): R11 - R20 (10个Region)

总已使用Region = 4(Eden) + 2(Survivor) + 4(Old) = 10个 > 9个,触发Mixed GC。

Mixed GC过程

  1. STW停顿开始。
  2. 它不仅会收集所有的Eden和Survivor Region(R1-R6)还会选择一部分“垃圾最多”的Old Region(比如R8和R10)进行回收
  3. 对选定Region进行标记-复制算法:存活的对象被复制到新的Region(通常是空闲的Old Region,如R13)。

Mixed GC后结果

Region 编号 Mixed GC前角色 Mixed GC后角色 变化说明
R1 - R4 Eden Old (空闲) 被清空,归还空闲列表。部分存活对象被复制到新的Survivor区。
R5, R6 Survivor (From) Old (空闲) 被清空,归还空闲列表。部分存活对象被复制到新的Survivor区。
R7, R9 Old (已用) Old (已用) 因为垃圾比例低未被选中回收。
R8, R10 Old (已用) Old (空闲) 因为垃圾比例高被选中回收,现已清空。
R11, R12 Old (空闲) Survivor (To) 成为新的Survivor区。
R13 Old (空闲) Old (已用) 存放本次GC中晋升的老年代对象、存放了从Old Region(R8,R10)中复制过来的存活对象。
R14 - R20 Old (空闲) Old (空闲) 保持不变。

最终总结与核心要点

  1. Survivor的创建Survivor区是在第一次Young GC时,为了容纳存活对象而动态创建的,而不是在JVM启动时就固定存在的。
  2. 动态分配:Eden区需要时从空闲池(由Old Region扮演)中分配,GC结束后再归还给空闲池。
  3. 角色的绝对动态性:没有任何Region是“专职”的。一个Region的生命周期典型路径是:
    空闲Old -> Eden -> (GC) -> 空闲Old -> Survivor -> (GC) -> 空闲Old -> Old (已用) -> (Mixed GC) -> 空闲Old
  4. 年轻代的“游走”:年轻代(Eden + Survivor)在物理上是一系列分散的、不断变化的Region的集合,它们在整个堆内存中“游走”。
  5. Mixed GC的精髓:它像Young GC一样工作,但“顺便”清理了一部分老年代的垃圾,从而避免了未来发生长时间Full GC的风险。
posted @ 2025-10-13 16:37  deyang  阅读(23)  评论(0)    收藏  举报