一次真实 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 设为相同值,以避免运行期扩容和预测失败带来的系统风险。

相关推荐
是一个Bug3 小时前
Java基础 -> JVM -> 并发 -> 框架 -> 分布式
java·jvm·分布式
高山上有一只小老虎20 小时前
如何下载并使用Memory Analyzer (MAT)
java·jvm
@淡 定21 小时前
JVM内存区域划分详解
java·jvm·算法
葛二蛋1 天前
JVM类加载过程:从字节码到运行时对象的诞生
jvm
violet-lz1 天前
C++ 内存分区详解
开发语言·jvm·c++
ss2731 天前
线程池优雅关闭:线程池生命周期管理:四种关闭策略的实战对比
java·jvm·算法
yyovoll1 天前
循环知识点介绍 -蓝桥杯
jvm·ide·java-ee
廋到被风吹走1 天前
【Java】【JVM】性能调优 监控指标、观测方法与问题解决方案
java·开发语言·jvm
@淡 定1 天前
JVM调优参数配置详解
java·jvm·算法