G1垃圾收集器
G1垃圾收集
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。
- 工作方式:
- 首先,G1会像Young GC一样回收所有年轻代Region。
- 同时,它会根据“Garbage-First”的原则,从老年代Region中选择垃圾最多(即存活对象最少)的几个Region加入到回收集合中一并回收。
- 这个过程会进行多次,直到回收了足够多的老年代垃圾(达到
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收集器相似的收集效果,混合收集才是核心,它有初始标记、并发标记、最终标记、筛选回收等阶段。
- 年轻代收集:类似 ParNew 的效果
- 核心:并行、STW的标记-复制算法。
- 目标:高效回收年轻代,高吞吐。
- 定位:常规的、高频发生的收集行为。
- 混合收集:G1 的核心与精髓
- 核心:这是一个完整的并发标记周期的成果,包含您提到的几个关键阶段:
- 初始标记:短暂的STW,标记GC Roots直接关联的对象。
- 并发标记:GC与用户线程并发执行,标记全堆存活对象。
- 最终标记:STW,处理并发阶段产生的引用变化。
- 筛选回收/清理:STW,统计垃圾并选择收益最高的老年代Region,准备混合收集。
- 目标:增量地、部分地回收老年代,实现可控的停顿时间和内存整理。
- 定位:G1实现其设计目标(低延迟、无碎片)的关键。
- 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(大部分情况)
- 触发:Eden区Region被填满。
- 行动:G1启动一次 Young GC。
- 回收范围:仅限年轻代Region(Eden和Survivor)。
- 结果:存活对象晋升到Survivor区或老年代Region。Young GC结束。应用线程继续运行。
- 此时,并发标记周期并未启动。这只是一次非常普通的、频繁发生的Young GC。
场景二:并发标记周期的触发(独立事件)
- 触发条件:这是一个独立的条件,与Young GC无直接关系。当整个堆的使用率达到
-XX:InitiatingHeapOccupancyPercent(IHOP) 设定的阈值时(例如,默认45%),G1会在后台启动并发标记周期。 - 触发时机:这个检查可能在一次Young GC之后、之前或期间发生。只要堆使用率达标,它就会触发。
- 行动:并发标记周期是一个漫长的、多阶段的后台任务,包括:
- 初始标记(STW,很短)
- 根区域扫描
- 并发标记
- 最终标记(STW)
- 清理(STW)
场景三:混合收集的触发(并发标记周期的结果)
- 触发条件:当且仅当并发标记周期完全结束后,G1才获得了足够的信息(知道了每个老年代Region的垃圾含量)。
- 行动:G1开始启动一系列 混合垃圾收集。
- 回收范围:每次Mixed GC都会:
- 首先,回收所有的年轻代Region(就像一次Young GC)。
- 同时,选择一部分垃圾最多(存活对象最少)的老年代Region加入回收集合进行回收。
Young GC到Mixed GC的流程
让我们用一段“故事”来描绘的过程:
- 第1-100次GC:应用不断运行,触发了很多次 Young GC。老年代逐渐被填满。
- 某个时刻:
jstat显示堆使用率达到了45%(IHOP阈值)。 - 触发事件:G1在后台启动了并发标记周期。这个周期可能持续几次Young GC的时间。
- 并发标记进行中:Young GC依然在因为Eden满而独立地、照常发生。
- 并发标记结束:标记周期完成了。现在G1有一个“垃圾密度”排行榜。
- 第101次及后续几次GC:从现在开始,接下来触发的几次GC不再是单纯的Young GC,而是 Mixed GC。每次Mixed GC都会在回收年轻代的同时,带走一些“垃圾排行榜”上名列前茅的老年代Region。
- Mixed GC系列结束:当回收的老年代空间足够多(达到
G1HeapWastePercent阈值),Mixed GC系列停止。 - 回到常态:之后的GC又变回了普通的 Young GC,直到堆使用率再次达到IHOP,开启下一个并发标记周期。
总结
- Young GC 和 并发标记周期 的触发是两个独立的循环。
- Young GC循环:
Eden满 -> Young GC -> Eden满 -> Young GC ... - 并发标记循环:
堆使用率 >= IHOP -> 启动并发标记 -> 标记完成 -> 启动Mixed GC系列 -> 回到起点
- Young 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
关键调优步骤
- 设定基线:使用上述推荐配置启动服务器。
- 收集数据:在模拟真实负载或压力测试下运行一段时间,收集 GC 日志。
- 分析日志:使用 GCeasy、G1GC Log Analyzer 或 JVM 内置工具 分析日志。
- 关注
MaxGCPauseMillis的实际达成情况。 - 关注是否有 "Evacuation Failure" 或 "Full GC"。
- 关注并发标记周期是否正常。
- 关注
- 迭代调优:
- 如果 年轻代 GC 太频繁:尝试增加
G1NewSizePercent。 - 如果 单次 GC 暂停太长:尝试降低
G1MaxNewSizePercent。 - 如果发生 "Evacuation Failure":尝试降低
IHOP或增加ConcGCThreads。 - 如果 吞吐量不足:可以适当放宽
MaxGCPauseMillis。
- 如果 年轻代 GC 太频繁:尝试增加
记住,没有放之四海而皆准的最优配置,最佳参数需要通过监控和分析你的特定应用负载来获得。
G1实例
核心思想:将堆划分为多个大小相等的 Region
- G1将整个Java堆(包括年轻代和老年代)划分为多个大小相等的独立内存区域,称为 Region。
- 每个Region在某一时刻,有且只有一个角色:Eden、Survivor 或 Old 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过程:
- STW停顿开始。
- 回收目标:只有 R1-R4 (所有Eden Region)。因为没有Survivor,所以不涉及Survivor的回收。
- 标记:从GC Roots开始,标记出Eden中所有存活的对象。
- 复制存活对象:
- 绝大多数新创建的对象都是“朝生夕死”的,会被直接回收。
- 极少数存活的对象需要被移动。由于这是第一次GC,JVM会从空闲列表(Old Region)中开辟一块或两块Region作为“To Survivor”。假设它开辟了 R5 和 R6。
- 所有存活对象被复制到新的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过程:
- 回收目标:R7-R10 (Eden) 和 R5, R6 (From Survivor)。
- 标记:标记这些Region中的所有存活对象。
- 复制存活对象:
- 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
触发条件:
- 系统继续运行。JVM会立即从空闲的Region中划出新的Eden Region供新对象分配。
- 很快,新的Eden Region又被填满,可能又经历了几次Young GC。每次Young GC都会导致Survivor区和老年代的使用率缓慢上升。
- 当整个堆的使用率超过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过程:
- STW停顿开始。
- 它不仅会收集所有的Eden和Survivor Region(R1-R6),还会选择一部分“垃圾最多”的Old Region(比如R8和R10)进行回收。
- 对选定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 (空闲) | 保持不变。 |
最终总结与核心要点
- Survivor的创建:Survivor区是在第一次Young GC时,为了容纳存活对象而动态创建的,而不是在JVM启动时就固定存在的。
- 动态分配:Eden区需要时从空闲池(由Old Region扮演)中分配,GC结束后再归还给空闲池。
- 角色的绝对动态性:没有任何Region是“专职”的。一个Region的生命周期典型路径是:
空闲Old->Eden->(GC)->空闲Old->Survivor->(GC)->空闲Old->Old (已用)->(Mixed GC)->空闲Old - 年轻代的“游走”:年轻代(Eden + Survivor)在物理上是一系列分散的、不断变化的Region的集合,它们在整个堆内存中“游走”。
- Mixed GC的精髓:它像Young GC一样工作,但“顺便”清理了一部分老年代的垃圾,从而避免了未来发生长时间Full GC的风险。
浙公网安备 33010602011771号