第八章 GC分代回收机制
概述
分代回收理论基础
JVM的垃圾回收机制基于分代假说理论(Generational Hypothesis),这一理论建立在以下两个观察基础之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的,即大部分对象在分配后很快就会变得不可达
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
基于这些假说,JVM将堆内存划分为不同的代,针对不同代的对象特点采用不同的回收策略:
- 新生代(Young Generation):存放新创建的对象,采用复制算法,回收频率高但速度快
- 老年代(Old Generation):存放长期存活的对象,采用标记-清除或标记-整理算法,回收频率低但耗时长
JVM内存模型
Java虚拟机(JVM)的内存模型是理解垃圾回收(GC)机制的基础。JVM内存模型主要分为堆(Heap)和非堆(Non-Heap)内存。
堆内存结构:
- 新生代(Young Generation):默认占堆内存的1/3(可通过-XX:NewRatio调整)
- Eden区:默认占新生代的8/10(可通过-XX:SurvivorRatio调整)
- Survivor0区:默认占新生代的1/10
- Survivor1区:默认占新生代的1/10
- 老年代(Old Generation/Tenured Generation):默认占堆内存的2/3
非堆内存:
- 元空间(Metaspace):JDK8+,存储类的元数据信息,替代永久代
- 程序计数器(PC Register):存储当前线程执行的字节码指令地址
- 本地方法栈(Native Method Stack):为本地方法服务
- 虚拟机栈(JVM Stack):存储局部变量、操作数栈等
GC的作用与重要性
垃圾回收(GC)是JVM自动管理内存的关键机制,其主要作用是回收不再使用的对象所占用的内存,从而避免内存泄漏和内存溢出问题。GC的重要性体现在以下几个方面:
- 内存管理自动化:程序员无需手动管理内存分配和释放,减少了内存泄漏和内存溢出的风险
- 提高系统稳定性:通过及时回收无用对象,确保程序有足够的内存空间运行,避免因内存不足而导致的程序崩溃
- 优化性能:合理的GC策略可以减少内存碎片,提高内存分配效率,从而提升程序的整体性能
本章将详细分析新生代和老年代的GC触发时机,并通过数据模拟展示GC过程中各区域的状态变化。
1. 新生代GC触发时机
1.1 Eden区满触发Minor GC
新生代的垃圾回收主要关注伊甸园(Eden)区和两个幸存者(Survivor)区。当Eden区被对象填满时,会触发Minor GC。以下是相关细节:
内存分配策略:
- 对象优先在Eden区分配:新创建的对象首先尝试在Eden区分配内存
- 大对象直接进入老年代:超过-XX:PretenureSizeThreshold阈值的对象直接分配到老年代(仅对Serial和ParNew收集器有效)
- 长期存活对象进入老年代:对象年龄达到-XX:MaxTenuringThreshold阈值(默认15)时晋升到老年代
- 动态对象年龄判定:如果Survivor区中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象直接晋升到老年代
触发条件:
- Eden区空间不足:当Eden区无法为新对象分配足够空间时立即触发
- 分配担保检查:在Minor GC前会检查老年代最大可用连续空间是否大于新生代所有对象总空间
- Survivor区溢出:当Survivor区无法容纳从Eden区和另一个Survivor区复制过来的存活对象时
回收机制:
- 标记阶段:从GC Roots开始,标记所有在Eden区和From Survivor区中的可达对象
- 复制阶段:将存活对象复制到To Survivor区,如果To Survivor区空间不足,则直接晋升到老年代
- 清理阶段:清空Eden区和From Survivor区的所有对象
- 角色交换:From Survivor和To Survivor角色互换
- 年龄增长:复制到Survivor区的对象年龄加1
- 晋升检查:检查对象是否满足晋升条件(年龄阈值或动态年龄判定)
性能影响:
- Minor GC的频率相对较高,但每次回收的时间相对较短
- 这是因为新生代中的对象大多数是临时对象,很快就会被回收
- 例如,在一个典型的Web应用中,Minor GC的平均耗时可能在10毫秒左右
1.2 Minor GC过程中的Survivor区溢出处理
Minor GC主要由Eden区空间不足触发,但在执行过程中可能遇到Survivor区溢出问题。以下是相关细节:
对象晋升机制:
- 在Minor GC过程中,Eden区和From Survivor区中的存活对象会被复制到To Survivor区
- 如果一个对象在多次Minor GC后仍然存活,它会被晋升到老年代
- 通常,经过15次Minor GC后仍然存活的对象会被晋升到老年代
Survivor区溢出(To-space overflow):
- 发生时机:在Minor GC的复制阶段,当JVM尝试将存活对象复制到To Survivor区时
- 溢出条件:To Survivor区的剩余空间不足以容纳所有需要复制的存活对象
- 示例:假设To Survivor区大小为10MB,而需要复制的存活对象总大小为12MB
- 关键理解:这是Minor GC执行过程中遇到的问题,而不是触发新Minor GC的条件
溢出处理机制:
- 强制晋升:JVM会立即将部分存活对象(通常是年龄较大的对象)直接晋升到老年代
- 空间分配担保检查:
- 检查老年代是否有足够连续空间容纳晋升对象
- 如果老年代空间足够,完成强制晋升,继续Minor GC
- 如果老年代空间不足,可能导致分配担保失败,触发Full GC
- 复制完成:晋升部分对象后,剩余对象复制到To Survivor区
处理流程:
- 检测溢出:复制阶段发现To Survivor区空间不足
- 选择晋升对象:根据年龄阈值或动态年龄判定选择对象
- 担保检查:验证老年代是否能容纳晋升对象
- 执行晋升:将选定对象直接移动到老年代
- 继续复制:将剩余对象复制到To Survivor区
- 清理原区域:清空Eden区和From Survivor区
性能影响:
- 停顿时间延长:强制晋升和担保检查会增加Minor GC的停顿时间
- 老年代压力增加:提前晋升的对象可能增加老年代的内存压力
- 潜在Full GC风险:如果担保失败,可能触发Full GC
- 典型耗时:在内存密集型应用中,这种情况下的Minor GC耗时可能达到20-50毫秒
1.3 动态年龄判断
当Survivor区中相同年龄的对象大小总和大于Survivor区空间的一半时,年龄大于等于该年龄的对象会直接进入老年代。
1.4 空间分配担保检查
在进行Minor GC之前,JVM会进行空间分配担保检查:
- 检查老年代最大可用连续空间是否大于新生代所有对象总空间
- 如果条件不满足且不允许担保失败,则会直接触发Full GC而非Minor GC
2. 老年代GC触发时机
2.1 老年代空间不足触发Full GC
老年代的垃圾回收主要关注对象的长期存活和内存空间的稳定性。当老年代的内存空间不足时,会触发Full GC。以下是相关细节:
触发条件:
- 老年代空间不足:当老年代无法为晋升对象或大对象分配足够空间时
- 分配担保失败:Minor GC时老年代无法担保所有可能晋升的对象
- 老年代使用率过高:当老年代使用率达到触发阈值时(具体阈值因收集器而异)
- 永久代/元空间满:JDK8之前的永久代或JDK8+的元空间内存不足
- 显式调用System.gc():程序显式请求垃圾回收(可通过-XX:+DisableExplicitGC禁用)
回收机制:
- Stop-The-World:暂停所有应用线程,确保回收过程的一致性
- 全堆回收:同时回收新生代和老年代的垃圾对象
- 标记-清除-整理:
- 标记阶段:从GC Roots开始标记所有可达对象
- 清除阶段:回收所有未标记的对象
- 整理阶段:压缩老年代空间,消除内存碎片
- 元空间回收:回收不再使用的类元数据(如果启用)
性能影响:
- 长时间停顿:Full GC的STW时间通常比Minor GC长10-100倍
- 应用暂停:所有应用线程被暂停,用户请求无法处理
- 吞吐量下降:频繁的Full GC会显著降低应用吞吐量
- 响应时间恶化:长时间的GC暂停导致响应时间不可预测
2.2 大对象直接分配到老年代触发Full GC
除了老年代空间不足触发Full GC外,大对象直接分配到老年代也可能触发Full GC。以下是相关细节:
大对象定义与处理:
- 大对象阈值:通过-XX:PretenureSizeThreshold设置(仅对Serial和ParNew收集器有效)
- 典型大对象:大数组、长字符串、大型集合对象
- 直接分配原因:避免在Eden区和Survivor区之间进行大量复制操作
- G1收集器:使用Humongous对象概念,超过region大小一半的对象被视为大对象
触发Full GC的条件:
- 老年代空间不足:大对象需要的连续空间超过老年代可用空间
- 内存碎片严重:老年代虽有足够总空间,但缺乏足够的连续空间
- 分配失败:大对象分配失败后触发Full GC尝试回收空间
处理机制:
- 空间检查:首先检查老年代是否有足够的连续空间
- 直接分配:如果空间足够,直接在老年代分配
- 触发Full GC:如果空间不足,触发Full GC释放空间
- 内存整理:Full GC过程中整理内存碎片,获得连续空间
- 重新尝试:Full GC后重新尝试分配大对象
- OOM检查:如果仍然无法分配,抛出OutOfMemoryError
性能影响:
- 正面影响:避免大对象在新生代的复制开销,减少Minor GC时间
- 负面影响:
- 可能频繁触发Full GC
- 加剧老年代内存碎片化
- 降低老年代空间利用率
- 优化建议:
- 合理设置PretenureSizeThreshold
- 考虑使用G1收集器处理大对象
- 优化应用避免创建过多大对象
2.3 空间分配担保失败
在Minor GC之前,JVM会检查老年代最大可用连续空间是否大于新生代所有对象的总空间:
- 如果大于,则Minor GC是安全的
- 如果小于,则查看HandlePromotionFailure设置
- 如果不允许冒险,则直接进行Full GC
2.4 System.gc()调用
程序显式调用System.gc()会建议JVM进行Full GC,但JVM可以忽略这个建议。
2.5 并发收集失败
在使用并发收集器(如CMS、G1)时,如果并发收集过程中老年代空间不足,会触发Full GC作为后备方案。
2.6 元空间不足
JDK8及以后版本中,当元空间(Metaspace)内存不足时,也会触发Full GC来尝试回收类元数据。
3. GC过程数据模拟
3.1 场景设置
假设我们有以下堆内存配置:
- 新生代总大小:30MB
- Eden区:24MB
- Survivor0(S0):3MB
- Survivor1(S1):3MB
- 老年代:70MB
- 对象晋升年龄阈值:15
3.2 初始状态
graph TB
subgraph "新生代 (30MB)"
E["Eden区<br/>24MB<br/>使用: 0MB"]
S0["Survivor0<br/>3MB<br/>使用: 0MB"]
S1["Survivor1<br/>3MB<br/>使用: 0MB"]
end
subgraph "老年代 (70MB)"
O["Old区<br/>70MB<br/>使用: 0MB"]
end
style E fill:#e1f5fe
style S0 fill:#e8f5e8
style S1 fill:#e8f5e8
style O fill:#fff3e0
3.3 第一次对象分配
分配20MB对象到Eden区:
graph TB
subgraph "新生代 (30MB)"
E["Eden区<br/>24MB<br/>使用: 20MB<br/>剩余: 4MB"]
S0["Survivor0<br/>3MB<br/>使用: 0MB"]
S1["Survivor1<br/>3MB<br/>使用: 0MB"]
end
subgraph "老年代 (70MB)"
O["Old区<br/>70MB<br/>使用: 0MB"]
end
style E fill:#ffcdd2
style S0 fill:#e8f5e8
style S1 fill:#e8f5e8
style O fill:#fff3e0
3.4 触发第一次Minor GC
当尝试分配6MB新对象时,Eden区空间不足,触发Minor GC:
GC前状态:
- Eden: 20MB(假设其中15MB对象存活)
- S0: 0MB
- S1: 0MB
- Old: 0MB
GC后状态:
graph TB
subgraph "新生代 (30MB)"
E["Eden区<br/>24MB<br/>使用: 6MB<br/>剩余: 18MB"]
S0["Survivor0<br/>3MB<br/>使用: 0MB"]
S1["Survivor1<br/>3MB<br/>使用: 3MB<br/>年龄1对象"]
end
subgraph "老年代 (70MB)"
O["Old区<br/>70MB<br/>使用: 12MB<br/>大对象直接晋升"]
end
style E fill:#ffcdd2
style S0 fill:#e8f5e8
style S1 fill:#ffcdd2
style O fill:#ffcdd2
3.5 多次GC后的状态演变
经过多次Minor GC后:
graph TB
subgraph "新生代 (30MB)"
E["Eden区<br/>24MB<br/>使用: 18MB<br/>剩余: 6MB"]
S0["Survivor0<br/>3MB<br/>使用: 2.5MB<br/>年龄2-8对象"]
S1["Survivor1<br/>3MB<br/>使用: 0MB"]
end
subgraph "老年代 (70MB)"
O["Old区<br/>70MB<br/>使用: 45MB<br/>晋升对象+大对象"]
end
style E fill:#ffcdd2
style S0 fill:#ffcdd2
style S1 fill:#e8f5e8
style O fill:#ffcdd2
3.6 触发Full GC的场景
当老年代空间不足时,触发Full GC:
Full GC前:
graph TB
subgraph "新生代 (30MB)"
E["Eden区<br/>24MB<br/>使用: 22MB"]
S0["Survivor0<br/>3MB<br/>使用: 0MB"]
S1["Survivor1<br/>3MB<br/>使用: 2.8MB"]
end
subgraph "老年代 (70MB)"
O["Old区<br/>70MB<br/>使用: 68MB<br/>剩余: 2MB"]
end
style E fill:#f44336
style S0 fill:#e8f5e8
style S1 fill:#f44336
style O fill:#f44336
Full GC后:
graph TB
subgraph "新生代 (30MB)"
E["Eden区<br/>24MB<br/>使用: 0MB<br/>清空"]
S0["Survivor0<br/>3MB<br/>使用: 0MB"]
S1["Survivor1<br/>3MB<br/>使用: 1.5MB<br/>存活对象"]
end
subgraph "老年代 (70MB)"
O["Old区<br/>70MB<br/>使用: 35MB<br/>压缩后"]
end
style E fill:#e1f5fe
style S0 fill:#e8f5e8
style S1 fill:#ffcdd2
style O fill:#ffcdd2
4. GC过程详细分析
4.1 Minor GC执行步骤
- 标记阶段:从GC Roots开始,标记所有可达对象
- 复制阶段:将Eden区和一个Survivor区的存活对象复制到另一个Survivor区
- 清理阶段:清空Eden区和原Survivor区
- 年龄增长:存活对象年龄+1
- 晋升判断:检查是否需要晋升到老年代
4.2 对象晋升条件
flowchart TD
A["新对象分配"] --> B{"对象大小 > PretenureSizeThreshold?"}
B -->|是| C["直接分配到老年代"]
B -->|否| D{"Eden区空间足够?"}
D -->|是| E["分配到Eden区"]
D -->|否| F["触发Minor GC"]
F --> G["复制存活对象"]
G --> H{"对象年龄 >= MaxTenuringThreshold?"}
H -->|是| I["晋升到老年代"]
H -->|否| J{"动态年龄判定满足?"}
J -->|是| I
J -->|否| K{"To Survivor空间足够?"}
K -->|是| L["复制到To Survivor"]
K -->|否| I
I --> M{"老年代空间足够?"}
M -->|是| N["完成晋升"]
M -->|否| O["触发Full GC"]
4.3 空间分配担保机制
flowchart TD
A["Minor GC前检查"] --> B{"老年代最大可用连续空间 > 新生代所有对象总空间?"}
B -->|是| C["担保成功,执行Minor GC"]
B -->|否| D{"老年代最大可用连续空间 > 历次晋升到老年代对象的平均大小?"}
D -->|是| E["担保成功,执行Minor GC"]
D -->|否| F["担保失败,执行Full GC"]
E --> G{"Minor GC后晋升对象大小 > 老年代剩余空间?"}
G -->|是| H["分配担保失败,执行Full GC"]
G -->|否| I["分配担保成功,GC完成"]
C --> J["执行Minor GC"]
J --> K{"所有对象都能正常晋升?"}
K -->|是| I
K -->|否| H
5. 性能优化建议
5.1 新生代调优
- Eden区大小:适当增大Eden区可以减少Minor GC频率
- Survivor比例:通过-XX:SurvivorRatio调整Eden与Survivor的比例
- 晋升阈值:通过-XX:MaxTenuringThreshold控制对象晋升时机
5.2 老年代调优
- 空间大小:确保老年代有足够空间容纳长期存活对象
- GC算法选择:根据应用特点选择合适的老年代GC算法
- 并发设置:合理配置并发GC参数
5.3 监控指标
- GC频率:Minor GC和Full GC的执行频率
- GC耗时:每次GC的停顿时间
- 内存使用率:各代的内存使用情况
- 对象晋升率:新生代到老年代的晋升速度
6. GC触发机制完整流程图
以下是涵盖所有可能GC触发情况的完整流程图:
flowchart TD
A["程序运行中"] --> B{"需要分配新对象?"}
B -->|是| C{"对象大小 > PretenureSizeThreshold?"}
B -->|否| Z["继续运行"]
C -->|是| D{"老年代连续空间足够?"}
C -->|否| E{"Eden区空间足够?"}
D -->|是| F["直接分配到老年代"]
D -->|否| G["触发Full GC"]
E -->|是| H["分配到Eden区"]
E -->|否| I["触发Minor GC前检查"]
I --> J{"分配担保检查"}
J -->|老年代可用空间 > 新生代所有对象| K["担保成功,执行Minor GC"]
J -->|否| L{"老年代可用空间 > 历次晋升平均大小?"}
L -->|是| M["担保成功,执行Minor GC"]
L -->|否| G
K --> N["执行Minor GC"]
M --> N
N --> O{"存活对象处理"}
O --> P{"对象年龄 >= MaxTenuringThreshold?"}
P -->|是| Q["晋升到老年代"]
P -->|否| R{"动态年龄判定"}
R -->|相同年龄对象 > Survivor空间一半| Q
R -->|否| S{"To Survivor空间足够?"}
S -->|是| T["复制到To Survivor"]
S -->|否| Q
Q --> U{"老年代空间足够?"}
U -->|是| V["晋升成功"]
U -->|否| W["分配担保失败,触发Full GC"]
G --> X["执行Full GC"]
W --> X
X --> Y{"Full GC后空间足够?"}
Y -->|是| AA["分配成功"]
Y -->|否| BB["OutOfMemoryError"]
T --> H
V --> H
F --> Z
H --> Z
AA --> Z
CC["System.gc()调用"] --> G
DD["元空间不足"] --> G
EE["并发收集失败"] --> G
FF["老年代使用率过高"] --> G
style G fill:#ff6b6b
style X fill:#ff6b6b
style W fill:#ff6b6b
style I fill:#4ecdc4
style N fill:#4ecdc4
style BB fill:#ff4757
7. GC触发时机对比分析
7.1 新生代vs老年代GC特点对比
| 特征 | 新生代GC (Minor GC) | 老年代GC (Full GC) |
|---|---|---|
| 触发频率 | 高 | 低 |
| 停顿时间 | 短(通常<50ms) | 长(可能>100ms) |
| 回收算法 | 复制算法 | 标记-清除/标记-整理 |
| 主要触发原因 | Eden区满、Survivor溢出 | 老年代空间不足、大对象分配 |
| 影响范围 | 仅新生代 | 整个堆内存 |
| 优化重点 | 减少对象晋升、合理配置新生代大小 | 减少Full GC频率、选择合适的收集器 |
7.2 GC触发时机优先级
- 最高优先级:OutOfMemoryError即将发生
- 高优先级:老年代空间严重不足(>95%)
- 中等优先级:Eden区满、大对象分配
- 低优先级:System.gc()调用、定期GC
8. 性能调优最佳实践
8.1 减少GC频率的策略
-
合理设置堆内存大小
- 新生代:老年代 = 1:2 或 1:3
- Eden:Survivor = 8:1:1
-
避免大对象频繁创建
- 使用对象池技术
- 分批处理大数据集
-
优化对象生命周期
- 及时释放不需要的引用
- 使用局部变量替代成员变量
8.2 监控关键指标
- GC频率:Minor GC < 1次/秒,Full GC < 1次/小时
- GC耗时:Minor GC < 50ms,Full GC < 200ms
- 内存使用率:老年代 < 80%,新生代周期性清空
- 对象晋升率:< 10MB/s
9. 总结
JVM的分代回收机制是一个复杂而精密的系统,它基于分代假说理论,通过将对象按生命周期分类管理,实现了高效的内存回收。本章详细分析了各种GC触发时机:
新生代GC(Minor GC)触发时机:
- Eden区空间不足是最主要的触发条件
- 分配担保检查确保Minor GC的安全性
- Survivor区溢出导致对象提前晋升到老年代
- 动态年龄判定优化对象晋升策略
老年代GC(Full GC)触发时机:
- 老年代空间不足无法容纳晋升对象
- 大对象分配失败(超过PretenureSizeThreshold且老年代空间不足)
- 分配担保失败(Minor GC前检查失败或Minor GC后晋升失败)
- 元空间不足、System.gc()调用、并发收集失败等特殊情况
关键要点:
- 分代假说理论:弱分代假说和强分代假说是分代回收的理论基础
- 精确的触发条件:理解各种GC触发的具体条件和检查机制
- 分配担保机制:Minor GC前的安全检查确保老年代能够容纳可能的晋升对象
- 对象晋升策略:年龄阈值和动态年龄判定共同决定对象何时晋升
- 性能调优原则:减少Full GC频率,优化Minor GC效率,合理配置内存参数
在实际应用中,需要根据具体的业务场景和性能要求,通过持续监控和测试来找到最优的GC配置参数。重要的是要理解不同收集器的特点:Serial和ParNew收集器支持PretenureSizeThreshold参数,而G1收集器使用不同的大对象处理机制。掌握这些基础的分代回收原理,将为深入学习现代垃圾收集器(如G1、ZGC、Shenandoah等)提供坚实的理论基础,也为实际项目中的GC调优提供科学的指导原则。