一次真实 GC 实验:Parallel 与 G1 在 `Xms < Xmx` 下的日志对比分析

文章目录

  • 一、实验环境说明
  • 二、实验代码(持续制造老年代压力)
  • [三、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)

三个铁证:

  1. 老年代容量:8MB → 19MB
  2. 堆总容量:16MB → 26MB
  3. 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,生产环境仍然推荐将 XmsXmx 设为相同值,以避免运行期扩容和预测失败带来的系统风险。

相关推荐
程序猿202313 小时前
MAT(memory analyzer tool)主要功能
jvm
期待のcode16 小时前
Java虚拟机的非堆内存
java·开发语言·jvm
jmxwzy20 小时前
JVM(java虚拟机)
jvm
Maỿbe20 小时前
JVM中的类加载&&Minor GC与Full GC
jvm
人道领域21 小时前
【零基础学java】(等待唤醒机制,线程池补充)
java·开发语言·jvm
小突突突21 小时前
浅谈JVM
jvm
饺子大魔王的男人1 天前
远程调试总碰壁?局域网成 “绊脚石”?Remote JVM Debug与cpolar的合作让效率飙升
网络·jvm
天“码”行空1 天前
java面向对象的三大特性之一多态
java·开发语言·jvm
独自破碎E2 天前
JVM的内存区域是怎么划分的?
jvm
期待のcode2 天前
认识Java虚拟机
java·开发语言·jvm