bigywater.tech
Java · 293

G1 Humongous Object:大对象为什么会改变回收节奏

G1 里多大的对象算「巨型」?它为什么浪费空间?没有引用之后要等并发周期才能回收吗?这篇用日志里的 Humongous Allocation 作为信号,逐个字节数地逼近阈值。

> 一半
对象大于区域一半才是巨型:524288 字节不是,524296 字节是(区域 1 MB)
200 MB
100 个 1126 KB 数组(有效数据 110 MB)在区域 1 MB 下的堆占用
7 次
分配 40 个 3 MB 数组时的暂停次数,全部是年轻代暂停,没有 Full GC

区域大小

实测 · 本机执行输出
  -Xmx64m  → Heap Region Size 1M
  -Xmx256m → Heap Region Size 1M
  -Xmx1g   → Heap Region Size 1M
  -Xmx4g   → Heap Region Size 2M
  -Xmx16g  → Heap Region Size 8M
  -Xmx1g -XX:G1HeapRegionSize=1m  → Heap Region Size 1M
  -Xmx1g -XX:G1HeapRegionSize=2m  → Heap Region Size 2M
  -Xmx1g -XX:G1HeapRegionSize=4m  → Heap Region Size 4M
  -Xmx1g -XX:G1HeapRegionSize=32m → Heap Region Size 32M
  G1HeapRegionSize=3m(不是 2 的幂)→ 4M

1 MB 区域的阈值

实测 · 本机执行输出
  区域 1m(阈值猜测 = 区域的一半 = 524288 字节);每个大小单独起一个 JVM,堆占用门槛调到 1%,保持引用地分配这种大小的 byte[] 直到共约 24 MB(数组对象总大小 = 下面的值,含 16 字节头):
    总大小   262144 字节( 25.00% 区域)→ 没有触发(普通对象)
    总大小   409600 字节( 39.06% 区域)→ 没有触发(普通对象)
    总大小   524224 字节( 49.99% 区域)→ 没有触发(普通对象)
    总大小   524280 字节( 50.00% 区域)→ 没有触发(普通对象)
    总大小   524288 字节( 50.00% 区域)→ 没有触发(普通对象)
    总大小   524296 字节( 50.00% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)
    总大小   524352 字节( 50.01% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)
    总大小   540672 字节( 51.56% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)
    总大小   614400 字节( 58.59% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)
    总大小  1048576 字节(100.00% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)

4 MB 区域的阈值

实测 · 本机执行输出
  区域 4m(阈值猜测 = 区域的一半 = 2097152 字节);每个大小单独起一个 JVM,堆占用门槛调到 1%,保持引用地分配这种大小的 byte[] 直到共约 24 MB(数组对象总大小 = 下面的值,含 16 字节头):
    总大小   614400 字节( 14.65% 区域)→ 没有触发(普通对象)
    总大小  1048576 字节( 25.00% 区域)→ 没有触发(普通对象)
    总大小  2097088 字节( 50.00% 区域)→ 没有触发(普通对象)
    总大小  2097144 字节( 50.00% 区域)→ 没有触发(普通对象)
    总大小  2097152 字节( 50.00% 区域)→ 没有触发(普通对象)
    总大小  2097160 字节( 50.00% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)
    总大小  2097216 字节( 50.00% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)
    总大小  2560000 字节( 61.04% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)
    总大小  4194304 字节(100.00% 区域)→ G1 Humongous Allocation 触发了并发周期(巨型对象)

空间浪费

实测 · 本机执行输出
      100 个 450 KB 数组:有效数据 43.9 MB;堆占用增量 47.9 MB(109%)
      100 个 1126 KB 数组:有效数据 110.0 MB;堆占用增量 200.0 MB(182%)

回收

实测 · 本机执行输出
  共 7 次暂停(40 个 3 MB 数组共约 120 MB,堆 64 MB);前 4 条:
     GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 26M->5M(64M) 0.344ms
     GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation) 25M->5M(64M) 0.182ms
     GC(4) Pause Young (Concurrent Start) (G1 Humongous Allocation) 25M->5M(64M) 0.228ms
     GC(6) Pause Young (Concurrent Start) (G1 Humongous Allocation) 25M->5M(64M) 0.210ms
    暂停类型汇总: ['Young(Concurrent Start,G1 Humongous Allocation)']
    没有 Full GC: True ;退出码 0
    gc+humongous 日志样例:
      GC(0) Humongous region 0 (object size 3145744 @ 0x0000000121800000) remset 0 code roots 0 marked 0 reclaim candidate 1 type array 1
      GC(0) Humongous region 8 (object size 3145744 @ 0x0000000122000000) remset 0 code roots 0 marked 0 reclaim candidate 1 type array 1
      GC(0) Humongous region 12 (object size 3145744 @ 0x0000000122400000) remset 0 code roots 0 marked 0 reclaim candidate 1 type array 1
      GC(0) Humongous region 4 (object size 3145744 @ 0x0000000121c00000) remset 0 code roots 0 marked 0 reclaim candidate 1 type array 1

分步演示

G1 巨型对象
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,G1,Apple Silicon;阈值的判断依据是日志里的 G1 Humongous Allocation(把 IHOP 调到 1%,每个大小单独一个 JVM)
    • 没有验证:并发周期里对巨型对象的处理、-XX:G1HeapRegionSize 对 Full GC 找不到连续区域时的失败(没有制造碎片)、JDK 27 的阈值、更大的区域(8M+)下的阈值、JDK 21 里没有找到关闭巨型对象提前回收的开关(PrintFlagsFinal 里查无此项),所以没有做关闭对照
    京ICP备2026067502号-1京公网安备11010502063487号