前言
“为什么我的Java应用突然变慢了?”、“什么又是Full GC?”,这几乎是每个Java开发者在职业生涯中都会遇到的“灵魂拷问”。要回答这些问题,我们必须深入到JVM的内存管理核心。本文将带你揭开老年代(Old Generation)的神秘面纱,弄清楚哪些对象会进入老年代,以及是什么触发了令人闻风丧胆的Full GC。
一、JVM的智慧:为何要有老年代?
在解释“什么”之前,我们先要理解“为什么”。JVM的内存管理基于一个重要的经验法则——弱分代假说(Weak Generational Hypothesis):
绝大多数对象都是“朝生夕死”的,生命周期极短。
熬过越多次垃圾回收过程的对象,就越难以消亡。
基于这个假说,JVM将堆内存划分成了两个主要区域:
- 新生代(Young Generation): 绝大多数新对象的出生地。它又细分为
Eden
区和两个Survivor
区(S0, S1)。这里的垃圾回收被称为 Minor GC,特点是频繁且快速。 - 老年代(Old Generation): 存放那些经过多次Minor GC后依然“坚强”存活下来的对象,以及一些特殊的大对象。这里的垃圾回收被称为 Major GC 或 Full GC,特点是不频繁但耗时。
这种分代设计,让JVM可以针对不同生命周期的对象采用不同的回收策略,从而极大地提升了回收效率。
二、哪些对象会“定居”在老年代?
一个对象从新生代到老年代,就像一场“升级打怪”之旅。以下是对象进入老年代的几种主要途径:
1. 正常晋升:年龄达标
这是最标准、最常见的晋升路径。
- 过程:
- 新对象在
Eden
区诞生。 - 当
Eden
区满,触发Minor GC
。存活的对象被移到Survivor
区,年龄(Age)计为1。 - 之后每经历一次
Minor GC
,存活的对象年龄就+1。 - 当对象的年龄达到一个阈值时,它就会被晋升到老年代。
- 新对象在
- 关键参数:
-XX:MaxTenuringThreshold=<N>
- 这个参数设定了晋升年龄的阈值,默认值通常是 15。
2. 动态年龄判定:一条捷径
JVM并不总是那么“死板”。它会根据 Survivor
区的使用情况动态调整晋升策略。
- 过程:在
Minor GC
后,JVM会检查Survivor
区。如果某一年龄及以下的所有对象总大小超过了Survivor
区空间的一半(默认50%),那么大于或等于这个年龄的所有对象都会被直接晋升到老年代。 - 关键参数:
-XX:TargetSurvivorRatio=<percent>
- 设定
Survivor
区使用率的阈值,默认为50。
- 设定
- 💡 举例:假设
Survivor
区大小为10MB,TargetSurvivorRatio
为50。GC后,年龄为1的对象占3MB,年龄为2的对象占3MB。此时3+3=6MB
已经超过了10MB * 50% = 5MB
,因此所有年龄>=2
的对象将立即晋升到老年代,无需等到15岁。
3. 大对象直通车:特殊通道
对于一些“庞然大物”(如大数组、长字符串),让它们在新生代来回复制是非常耗费性能的。因此,JVM为它们开辟了“绿色通道”。
- 过程:当一个对象的大小超过了JVM设定的阈值,它将不会在新生代分配,而是直接在老年代分配。
- 关键参数:
-XX:PretenureSizeThreshold=<size_in_bytes>
- 定义大对象的阈值。注意:此参数对
G1
等收集器不生效,G1有自己的Humongous
区域处理大对象。
- 定义大对象的阈值。注意:此参数对
4. 空间分配担保:最后的保障
这是一种兜底机制,防止新生代GC后,存活对象太多导致 Survivor
区装不下的窘境。
- 过程:在
Minor GC
之前,JVM会检查老年代的可用空间是否足够容纳新生代所有对象。如果不够,会进行一系列检查。如果最终判断Survivor
区可能无法容纳所有存活对象,这些对象可能会被直接移入老年代。如果老年代也放不下,就会触发一次Full GC
。
晋升老年代途径总结
晋升场景 | 核心原因 | 相关JVM参数 |
---|---|---|
年龄判定 | 对象在新生代中经历了足够多次的GC,被认为是“长期存活”的对象。 | -XX:MaxTenuringThreshold |
动态年龄计算 | Survivor区中同龄对象总和超过一定比例,为避免空间不足,提前晋升。 | -XX:TargetSurvivorRatio |
大对象直通车 | 对象体积过大,在新生代复制成本高,因此直接在老年代分配。 | -XX:PretenureSizeThreshold |
空间分配担保 | Minor GC后,存活对象太多,Survivor区放不下,只能移入老年代。 | (一种兜底机制) |
三、GC的分类与“大扫除”Full GC的触发时机
理解了对象如何进入老年代,我们再来看看GC的执行时机,尤其是性能杀手——Full GC。
GC类型简介
- Minor GC / Young GC:只回收新生代。发生在Eden区满时,速度快,STW(Stop-The-World)时间短。
- Full GC:回收整个堆(新生代+老年代)以及元空间(Metaspace)。速度慢,STW时间长,是性能调优的重点关注对象。
🎯 什么时候会发生 Full GC?
-
老年代空间不足(最常见)
- 当对象从新生代晋升,或大对象要直接分配时,如果老年代剩余空间不足,就会触发
Full GC
。这是最典型的触发场景。
- 当对象从新生代晋升,或大对象要直接分配时,如果老年代剩余空间不足,就会触发
-
元空间/永久代空间不足
Metaspace
(JDK 8+)或PermGen
(JDK 7-)用于存储类的元数据。当加载的类过多,导致这块区域满了,JVM会触发Full GC
来卸载不用的类。- 相关参数:
-XX:MetaspaceSize
、-XX:MaxMetaspaceSize
-
显式调用
System.gc()
- 代码中手动调用
System.gc()
会建议JVM执行Full GC
。 -
警告:在生产环境中,强烈不推荐手动调用
System.gc()
,因为它会带来极大的性能不确定性。可以通过-XX:+DisableExplicitGC
参数来禁止它。
- 代码中手动调用
-
空间分配担保失败
- 如前所述,在
Minor GC
前的检查阶段,如果JVM判断老年代无法安全地容纳此次Minor GC
后可能晋升的对象,就可能会提前触发一次Full GC
来整理空间。
- 如前所述,在
四、总结:一张图看懂GC对比
GC 类型 | 回收区域 | 触发时机 | 特点 | 对性能影响 |
---|---|---|---|---|
Minor GC | 新生代 | Eden 区满 | 速度快,频率高 | 小,STW时间短 |
Full GC | 整个堆 + 元空间 | 1. 老年代空间不足 2. 元空间不足 3. 显式调用 System.gc() 4. 空间分配担保失败 |
速度慢,频率低 | 大,STW时间长,是性能瓶颈的主要来源 |
五、核心要点与性能调优启示
- 分代是为了高效:通过区分对象的生命周期,实现快速、低影响的垃圾回收。
- 老年代是“养老院”:存放长期存活的对象和大对象。
- Full GC是“性能杀手”:它的
Stop-The-World
时间是应用卡顿的主要元凶。
因此,所有JVM性能调优的最终目标之一,都是尽可能减少Full GC的频率和持续时间。这通常需要我们合理配置堆内存大小、新生代与老年代的比例,并优化代码以避免内存泄漏和创建过多不必要的大对象。