GC什么时候来收垃圾?这不是随机的,而是有严格的触发条件!让我们一起揭秘~
🎬 开场:GC的两种模式
想象一个小区的垃圾清理 🏘️
Young GC(年轻代GC) = 日常清理 🧹
- 每天清理垃圾桶(Eden区)
- 速度快,几十毫秒搞定
- 频率高,一天多次
Full GC(全堆GC) = 大扫除 🧽
- 整个小区大扫除(整个堆)
- 速度慢,可能几秒钟
- 频率低,但一次很累
时间轴:
Young GC: ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
|小|小|小|小|小|小|小|小|小|小|
|扫|扫|扫|扫|扫|扫|扫|扫|扫|扫|
Full GC: ↓
|===大扫除===|
(所有人都停下)
🎯 Young GC的触发条件
条件1:Eden区满了 📦
这是最常见的触发条件!
堆内存结构:
┌─────────────────────────────────────┐
│ Young Generation │
│ ┌──────────┬──────┬──────┐ │
│ │ Eden │ S0 │ S1 │ │
│ │ ████████ │ │ ████ │ │
│ │ 满了! │ │ │ │
│ └──────────┴──────┴──────┘ │
│ ↓ │
│ 触发Young GC! │
└─────────────────────────────────────┘
工作流程:
// 每次new对象都在Eden区分配
User user = new User(); // 在Eden分配
Order order = new Order(); // 在Eden分配
...
// 当Eden区空间不足时
new BigObject(); // Eden放不下了!
↓
触发Young GC
↓
清理Eden区的垃圾
↓
存活对象移到Survivor区
详细过程:
GC前:
Eden区: [对象A] [对象B] [对象C] [对象D] [对象E] ← 满了
S0区: [对象X(age=1)]
S1区: 空
GC中(复制算法):
1. 扫描Eden + S0区
2. 标记存活对象:B、D、X
3. 复制到S1区:
- B(age=1)
- D(age=1)
- X(age=2)
GC后:
Eden区: 空 ← 清空了
S0区: 空 ← 清空了
S1区: [B(age=1)] [D(age=1)] [X(age=2)]
条件2:分配担保失败 🎰
场景:
1. Young GC前,检查老年代剩余空间
2. 如果老年代空间 < 历史晋升到老年代的平均大小
3. 提前触发Full GC(为了安全)
为什么?
- Young GC时,存活对象可能晋升到老年代
- 如果老年代空间不够,就会失败
- 所以提前做Full GC腾出空间
🔥 Full GC的触发条件
条件1:老年代空间不足 🏚️
场景1:大对象直接进老年代
// 创建超大对象
byte[] bigArray = new byte[10 * 1024 * 1024]; // 10MB
// 这个对象太大,直接分配到老年代
// 如果老年代空间不足
↓
触发Full GC
大对象阈值:
# JVM参数
-XX:PretenureSizeThreshold=3145728 # 3MB以上直接进老年代
场景2:长期存活对象晋升
对象年龄增长:
Young GC次数: 1 2 3 4 5 ... 15
对象年龄: age1 age2 age3 age4 age5 ... age15
↓
晋升到老年代
当老年代满了:
Old区: ████████████████████████ ← 满了!
↓
触发Full GC
晋升年龄阈值:
-XX:MaxTenuringThreshold=15 # 默认15次Young GC后晋升
条件2:动态年龄判断 🎂
不一定要等到15岁才晋升!
动态年龄判断规则:
如果 Survivor区中,相同年龄的所有对象大小总和 > Survivor空间的50%
那么:年龄 >= 该年龄的对象直接进入老年代
举例:
Survivor总大小:10MB
age=1的对象:2MB
age=2的对象:3MB
age=3的对象:4MB ← 前面累加已经 > 5MB (50%)
age=4的对象:2MB
结果:age >= 3的对象全部晋升到老年代!
条件3:元空间(Metaspace)不足 📚
元空间用途:
- 存储类的元数据
- 类加载器加载的类信息
- 常量池
元空间满了:
Metaspace: ████████████ ← 达到阈值
↓
触发Full GC
↓
回收无用的类
典型场景:
// 场景1:大量使用CGLIB代理
for (int i = 0; i < 100000; i++) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyClass.class);
enhancer.setCallback(...);
Object proxy = enhancer.create(); // 每次生成新类
}
// 生成10万个代理类 → 元空间爆满 → Full GC
// 场景2:频繁加载和卸载类(如热部署)
ClassLoader loader = new MyClassLoader();
Class<?> clazz = loader.loadClass("com.example.MyClass");
// ... 使用
loader = null; // 卸载类加载器
// 触发Full GC回收类元数据
相关参数:
# 元空间大小设置
-XX:MetaspaceSize=128m # 初始大小
-XX:MaxMetaspaceSize=256m # 最大大小
-XX:MinMetaspaceFreeRatio=40 # 最小空闲比例
-XX:MaxMetaspaceFreeRatio=70 # 最大空闲比例
条件4:System.gc() 调用 🎮
// 程序主动调用
System.gc(); // 建议JVM进行Full GC
// 注意:这只是"建议",不是"命令"
// JVM可以选择忽略
实际场景:
// ❌ 不好的做法
public void processLargeData() {
// 处理大量数据
List<BigData> dataList = loadBigData();
process(dataList);
dataList = null;
System.gc(); // 想立即回收内存,但这样不好!
}
// ✅ 正确做法
public void processLargeData() {
// 让JVM自己决定何时GC
List<BigData> dataList = loadBigData();
process(dataList);
// dataList在方法结束后自然会被回收
}
禁用System.gc():
# 生产环境建议禁用
-XX:+DisableExplicitGC
条件5:空间分配担保失败 🎯
什么是空间分配担保?
Young GC前的检查机制:
1. JVM检查:老年代最大连续可用空间 > 新生代所有对象总大小
✅ 是 → 安全,执行Young GC
❌ 否 → 继续检查
2. JVM检查:老年代最大连续可用空间 > 历次晋升到老年代的对象平均大小
✅ 是 → 冒险尝试Young GC
❌ 否 → 改为Full GC(保险起见)
图解:
场景1:空间足够
Young: 100MB (对象总大小)
Old: 200MB (可用空间)
↓
200MB > 100MB ✅
↓
执行Young GC
场景2:空间不足,但历史数据OK
Young: 100MB
Old: 50MB (可用空间)
历史平均晋升: 30MB
↓
50MB < 100MB ❌
但 50MB > 30MB ✅ (历史平均)
↓
冒险执行Young GC
场景3:太危险了
Young: 100MB
Old: 50MB
历史平均晋升: 80MB
↓
50MB < 80MB ❌
↓
改为Full GC(安全第一)
相关参数:
# JDK 6 Update 24之前
-XX:+HandlePromotionFailure=true # 允许担保失败
# JDK 6 Update 24之后
# 这个参数被废弃,JVM自动处理
条件6:并发模式失败(CMS专属)🏃♂️
CMS GC的并发问题:
CMS的并发标记阶段:
1. CMS在后台并发回收老年代
2. 同时,应用程序继续运行
3. 应用程序继续产生新对象
问题场景:
Old区: ████████░░░░ (正在GC中)
↓ CMS正在清理
↓ 但应用继续分配对象
Old区: ██████████░░ (新对象进来了)
↓ CMS还没清理完
↓ 应用又分配对象
Old区: ████████████ (满了!)
↓
Concurrent Mode Failure!
↓
降级为Serial Old (STW)
↓
停顿时间巨长!💥
解决方案:
# 提前触发CMS GC
-XX:CMSInitiatingOccupancyFraction=70 # 老年代70%时触发CMS
-XX:+UseCMSInitiatingOccupancyOnly # 只用这个阈值
# 增大老年代
-Xmx4g # 增大堆大小
-XX:NewRatio=2 # 老年代是新生代的2倍
📊 GC触发条件总结表
| GC类型 | 触发条件 | 频率 | 停顿时间 | 影响范围 |
|---|---|---|---|---|
| Young GC | Eden区满 | 高 | 几十ms | 新生代 |
| Full GC | 老年代满 | 低 | 几百ms-几秒 | 整个堆+元空间 |
| Full GC | 元空间满 | 很低 | 几百ms | 整个堆+元空间 |
| Full GC | System.gc() | 取决于代码 | 几百ms | 整个堆 |
| Full GC | 空间担保失败 | 低 | 几百ms | 整个堆 |
| Full GC | CMS失败 | 罕见 | 几秒 | 老年代 |
🎪 实战案例分析
案例1:频繁Young GC 🔥
现象:
# jstat监控
jstat -gcutil <pid> 1000
S0 S1 E O M YGC YGCT FGC
0.00 50.00 99.99 30.00 90.00 1000 50.00 5
0.00 50.00 99.99 30.05 90.00 1001 50.05 5
0.00 50.00 99.99 30.10 90.00 1002 50.10 5
# 问题:
# - E(Eden区)一直在99.99%
# - YGC(Young GC次数)疯狂增长
# - 1秒就触发1次Young GC!
原因分析:
// 问题代码
@RestController
public class UserController {
@GetMapping("/users")
public List<User> getUsers() {
// 每次请求都创建10万个对象
List<User> users = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
users.add(new User("user" + i)); // 疯狂创建对象
}
return users;
}
}
// 问题:
// - Eden区太小,放不下这么多对象
// - 每次请求都触发Young GC
解决方案:
# 方案1:增大新生代
-Xmn512m # 新生代512MB
# 方案2:调整新生代比例
-XX:NewRatio=2 # 老年代:新生代 = 2:1
# 方案3:优化代码(最根本)
@GetMapping("/users")
public List<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "100") int size) {
// 分页查询,不要一次加载所有数据
return userService.getUsers(page, size);
}
案例2:频繁Full GC 💥
现象:
jstat -gcutil <pid> 1000
S0 S1 E O M YGC FGC FGCT
0.00 0.00 20.00 99.99 95.00 100 50 250.00
0.00 0.00 25.00 99.99 95.00 100 51 255.00
0.00 0.00 30.00 99.99 95.00 100 52 260.00
# 问题:
# - O(老年代)一直99.99%
# - FGC(Full GC次数)疯狂增长
# - FGCT(Full GC总耗时)已经250秒!
可能原因:
原因1:内存泄漏
// 问题代码
public class CacheService {
private static Map<String, byte[]> cache = new HashMap<>();
public void addCache(String key) {
// 不断添加,never清理
cache.put(key, new byte[1024 * 1024]); // 每次1MB
}
}
// 结果:老年代被占满,频繁Full GC
原因2:老年代太小
# 查看当前配置
java -XX:+PrintFlagsFinal -version | grep HeapSize
# 发现:
InitialHeapSize = 256MB # 太小了!
MaxHeapSize = 256MB
# 解决:增大堆大小
-Xms2g -Xmx2g
原因3:大对象太多
// 问题代码
public void processData() {
// 创建大量大对象
for (int i = 0; i < 1000; i++) {
byte[] data = new byte[5 * 1024 * 1024]; // 5MB
process(data);
}
}
// 5MB的对象直接进老年代
// 1000个对象 = 5GB → 老年代爆满
🎯 GC调优建议
1. Young GC优化 🎯
# 目标:降低Young GC频率和停顿时间
# 策略1:增大新生代
-Xmn1g # 新生代1GB
# 策略2:调整Eden和Survivor比例
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
# 策略3:使用G1 GC(自动调整)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标停顿时间200ms
2. Full GC优化 🎯
# 目标:减少或避免Full GC
# 策略1:增大老年代
-Xmx4g # 总堆4GB
-XX:NewRatio=2 # 老年代占2/3
# 策略2:调整晋升阈值
-XX:MaxTenuringThreshold=15 # 延迟晋升
# 策略3:使用CMS或G1
-XX:+UseConcMarkSweepGC # CMS GC
-XX:+UseG1GC # G1 GC
# 策略4:元空间调优
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
# 策略5:禁用System.gc()
-XX:+DisableExplicitGC
3. 监控和诊断 📊
# 启用GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/path/to/gc.log
# 使用GC日志分析工具
# - GCEasy (https://gceasy.io)
# - GCViewer
# - JProfiler
🎓 总结:GC触发条件口诀
┌────────────────────────────────────┐
│ Young GC触发条件 │
├────────────────────────────────────┤
│ 1. Eden区满了(最常见)✅ │
│ 2. 空间分配担保检查 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ Full GC触发条件 │
├────────────────────────────────────┤
│ 1. 老年代空间不足 🏚️ │
│ 2. 元空间不足 📚 │
│ 3. System.gc()调用 🎮 │
│ 4. 空间分配担保失败 🎯 │
│ 5. CMS并发模式失败 🏃♂️ │
│ 6. 晋升失败(Promotion Failed) │
└────────────────────────────────────┘
记住三句话:
-
Young GC是常态,Full GC是异常 🚨
- Young GC频繁很正常
- Full GC频繁说明有问题
-
Eden满触发Young GC,老年代满触发Full GC 🎯
- 最基本的触发条件
-
优化目标:减少Full GC,缩短停顿时间 ⏱️
- Full GC才是性能杀手
下次面试官问GC触发条件,你就说:
"Young GC主要在Eden区满时触发,采用复制算法,速度快,停顿时间短。Full GC的触发条件更复杂:老年代空间不足、元空间不足、System.gc()调用、空间分配担保失败都可能触发。Full GC会回收整个堆,停顿时间长,是性能优化的重点。生产环境要避免频繁Full GC,可以通过增大堆内存、优化代码减少大对象创建、使用G1 GC等方式优化!" 🎓
🎉 掌握GC触发条件,性能调优不再迷茫! 🎉