分代收集与对象晋升
分代收集是 HotSpot JVM 长期使用的重要思想。它基于“多数对象很快死亡,少数对象长期存活”的经验规律。
# 1. 新生代
新生代通常存放新创建的对象。
传统布局:
Eden + Survivor From + Survivor To
对象优先在 Eden 分配。当 Eden 空间不足时触发 Young GC。
# 2. Survivor 区
Young GC 时,Eden 和一个 Survivor 区中的存活对象会被复制到另一个 Survivor 区。
Eden + From -> To
回收后,From 和 To 角色互换。
Survivor 区的意义是让短期存活对象有机会在新生代继续观察,而不是立刻进入老年代。
# 3. 对象年龄
对象每经历一次 Young GC 后仍然存活,年龄会增加。
当年龄达到阈值后,对象可能晋升到老年代。
相关参数:
-XX:MaxTenuringThreshold
实际晋升并不只看这个参数,还会受 Survivor 空间和动态年龄判断影响。
# 4. 老年代
老年代存放生命周期较长的对象。
对象进入老年代的常见原因:
- 年龄达到晋升阈值。
- Survivor 空间不足。
- 大对象直接进入老年代。
- 动态年龄判断触发晋升。
老年代回收通常比新生代成本更高,停顿也可能更明显。
# 5. 大对象
大对象是指需要大量连续内存的对象,例如大数组。
byte[] data = new byte[20 * 1024 * 1024];
大对象可能直接进入老年代,避免在新生代复制来复制去。
大对象频繁创建容易导致:
- 老年代压力大。
- 内存碎片。
- Full GC。
- 分配失败。
# 6. 空间分配担保
Young GC 前,JVM 需要判断老年代是否有足够空间容纳可能晋升的对象。
如果担保失败,可能提前触发老年代回收或 Full GC。
这就是为什么新生代回收也可能牵动老年代。
# 7. G1 中的分代思想
G1 使用 Region 管理堆,不再是连续的新生代和老年代。
但 G1 仍有分代概念:
- Eden Region。
- Survivor Region。
- Old Region。
只是这些 Region 在物理上可以不连续。
# 8. 阶段小结
分代收集的核心是根据对象生命周期选择不同策略。新对象在 Eden 分配,存活对象进入 Survivor,多次存活后晋升老年代。理解对象晋升路径,有助于分析 Young GC、老年代增长和 Full GC。
# 9. 新生代复制流程
Young GC 前:
Eden: [A][垃圾][B][垃圾]
From: [C][垃圾]
To: [空][空][空]
Young GC 后:
Eden: [空][空][空][空]
From: [空][空]
To: [A age+1][B age+1][C age+1]
下一轮:
From 和 To 交换角色
每次 Young GC 后,存活对象年龄增加。如果达到晋升阈值,或 Survivor 放不下,就进入老年代。
# 10. 动态年龄判断
对象不一定非要达到 MaxTenuringThreshold 才晋升。
JVM 可能根据 Survivor 中相同年龄对象大小做动态判断:如果某个年龄及以上对象大小总和超过 Survivor 空间的一定比例,这些对象可能提前晋升。
这就是为什么有时明明设置了较高晋升年龄,对象还是很快进入老年代。
# 11. 晋升过快的排查方向
| 现象 | 可能原因 |
|---|---|
| Young GC 后老年代明显增长 | 中生命周期对象多 |
| Survivor 使用率很高 | Survivor 太小或存活对象多 |
| Full GC 变频繁 | 晋升速度超过老年代回收能力 |
| 大对象直接进老年代 | 数组、字符串、批量查询过大 |
优化方向:
- 减少一次请求中的大对象。
- 控制批量查询大小。
- 避免大 JSON 一次性反序列化。
- 调整新生代和 Survivor,但要基于日志。
# 12. Tips 快问快答
Q:对象年龄存在哪里?
A:HotSpot 中对象年龄信息存放在对象头 Mark Word 中的一部分位上。
Q:为什么 Survivor 有两个?
A:复制算法需要 From 和 To 两个区域,在 Young GC 时把存活对象复制到另一块。
Q:大对象为什么可能直接进老年代?
A:大对象复制成本高,也可能需要连续空间,因此 JVM 可能让它直接进入老年代或 Humongous Region。
Q:Young GC 会不会导致 Full GC?
A:可能。比如晋升担保失败、老年代空间不足等场景可能引发更重的回收。