文章目录
- 一、实验环境说明
- 二、实验代码(持续制造老年代压力)
- [三、Parallel GC:扩容 = Full GC(非常直接)](#三、Parallel GC:扩容 = Full GC(非常直接))
- [四、G1 GC:扩容"更温和",但不是没有代价](#四、G1 GC:扩容“更温和”,但不是没有代价)
- [五、Parallel vs G1:日志级别对比总结](#五、Parallel vs G1:日志级别对比总结)
- [六、为什么两种 GC 都推荐 Xms = Xmx](#六、为什么两种 GC 都推荐 Xms = Xmx)
很多 JVM 文章都会告诉你:
生产环境要设置-Xms = -Xmx但很少有人用真实 GC 日志 告诉你:
如果不这么做,Parallel 和 G1 分别会发生什么?
本文基于 Java 17 ,使用同一份代码、同一组内存参数 ,
对比 Parallel GC 与 G1 GC 在
Xms < Xmx场景下的真实行为差异。
一、实验环境说明
JVM 版本
text
JDK 17.0.7
JVM 参数(仅 GC 不同)
Parallel GC
bash
-Xms16m -Xmx64m -Xmn8m
-XX:+UseParallelGC
-Xlog:gc*,gc+heap=info
G1 GC
bash
-Xms16m -Xmx64m
-XX:+UseG1GC
-Xlog:gc*,gc+heap=info
二、实验代码(持续制造老年代压力)
java
public class HeapExpansionFullGCDemo {
static class BigObject {
byte[] data = new byte[1024 * 1024]; // 1MB
}
public static void main(String[] args) {
List<Object> holder = new ArrayList<>();
while (true) {
holder.add(new BigObject());
}
}
}
代码特征
- 持续创建 1MB 大对象
- 使用强引用保存
- 极易触发老年代压力
- 在 G1 下属于 Humongous Object
三、Parallel GC:扩容 = Full GC(非常直接)
1️⃣ 初始阶段:堆大小固定在 Xms
text
GC(0) Pause Young (Allocation Failure)
5M->4M(15M)
说明此时堆容量 ≈ 16MB(Xms)
2️⃣ 第一次扩容:伴随 Full GC(关键证据)
text
GC(1) Pause Full (Ergonomics)
ParOldGen: 4104K(8192K)->4867K(19968K)
4M->4M(26M)
三个铁证:
- 老年代容量:
8MB → 19MB - 堆总容量:
16MB → 26MB - Full GC 原因:
Ergonomics
👉 说明 JVM 在运行期扩容,并且扩容必须通过 Full GC 完成
3️⃣ 多次扩容 + 多次 Full GC
随着对象持续创建:
text
ParOldGen: 15107K(19968K)->15107K(36864K)
Pause Full (Ergonomics)
text
ParOldGen: 35588K(36864K)->35588K(57344K)
Pause Full (Ergonomics)
特点非常明显:
- 每次扩容
- 都伴随 Full GC
- Full GC 成为"常规操作"
4️⃣ 最终结局:Allocation Failure + OOM
text
Pause Full (Allocation Failure)
ParOldGen used 99%
java.lang.OutOfMemoryError
Parallel GC 的完整路径
Xms < Xmx
→ 运行期扩容
→ 扩容 = Full GC
→ Full GC 频繁
→ 老年代耗尽
→ OOM
四、G1 GC:扩容"更温和",但不是没有代价
1️⃣ 一开始就不一样:Humongous Object
text
Heap Region Size: 1M
Pause Young (G1 Humongous Allocation)
Humongous regions: 8->8
说明:
- 1MB 对象 ≥ 0.5 Region
- 直接进入 Humongous Region
- 直接占用 Old Region
2️⃣ G1 的扩容方式:Region 逐步启用
你在日志中看到的是:
text
9M->8M(16M)
14M->14M(29M)
34M->34M(62M)
58M->58M(64M)
而不是:
text
Heap expanded from A to B
这意味着:
- G1 在运行期逐步启用 Region
- 堆容量"平滑增长"
- 没有立即触发 Full GC
👉 这正是 G1 相比 Parallel 的优势
3️⃣ 前期:只有 Young / Concurrent GC,没有 Full GC
text
Pause Young (G1 Evacuation Pause)
Concurrent Mark Cycle
Pause Remark
Pause Cleanup
G1 通过并发标记和预防性回收 成功顶住压力。
4️⃣ 真正的转折点:Humongous Region 失控
text
Humongous regions: 60->60
含义:
- 约 60MB 的堆
- 几乎全被 Humongous Object 占满
- Old Region 严重碎片化
5️⃣ G1 宣布"兜底":Full GC(Compaction)
text
Attempting full compaction
Pause Full (G1 Compaction Pause)
随后:
text
Attempting maximum full compaction clearing soft references
Pause Full (G1 Compaction Pause)
这是 G1 的最后手段。
6️⃣ 最终结果:Full GC 失败 → OOM
text
java.lang.OutOfMemoryError: Java heap space
G1 的完整路径
Xms < Xmx
→ Region 逐步扩容
→ 并发 / Mixed GC
→ Humongous Region 占满
→ G1 Compaction Full GC
→ OOM
五、Parallel vs G1:日志级别对比总结
| 对比项 | Parallel GC | G1 GC |
|---|---|---|
| 扩容方式 | 整堆重布局 | 启用更多 Region |
| 扩容是否 STW | 是 | 是(但更轻) |
| 扩容是否触发 Full GC | 几乎必然 | 通常不会 |
| Full GC 角色 | 常规手段 | 最终兜底 |
| Humongous 对象 | 无特殊处理 | 极易致命 |
| OOM 前信号 | 多次 Full GC | Concurrent → Compaction |
六、为什么两种 GC 都推荐 Xms = Xmx
基于这两份真实日志,可以得出一个非常清晰的结论:
- 在 Parallel GC 下,
Xms < Xmx几乎等价于
运行期 Full GC 风险- 在 G1 GC 下,虽然扩容更温和,
但在高分配速率或 Humongous Object 场景中,
仍然可能退化为 Compaction Full GC👉 因此,即使使用 G1,生产环境仍然推荐将
Xms与Xmx设为相同值,以避免运行期扩容和预测失败带来的系统风险。