bigywater.tech
Java · 239

Compressed Oops:对象引用为什么可以用更少位数

为什么堆设成 32 GB 以上对象反而变大?压缩指针的边界在哪里,关掉它每个对象多出多少字节?这篇用 PrintFlagsFinal、堆地址日志和分配字节数把这些数字逐个量出来,并对照 JDK 27 默认开启的紧凑对象头。

31900m / 32g
压缩指针开启的最大堆 / 被关闭的最小堆(对象对齐 8 字节)
155 → 194 MB
500 万个节点的堆占用:开启 → 关闭压缩指针(JDK 21)
8 字节
JDK 27 默认配置下 new Object() 的大小(紧凑对象头)

-Xmx 与压缩指针开关

实测 · 本机执行输出
  -Xmx1g      MaxHeapSize=1024 MB  UseCompressedOops=true  UseCompressedClassPointers=true  ObjectAlignmentInBytes=8
  -Xmx4g      MaxHeapSize=4096 MB  UseCompressedOops=true  UseCompressedClassPointers=true  ObjectAlignmentInBytes=8
  -Xmx16g     MaxHeapSize=16384 MB  UseCompressedOops=true  UseCompressedClassPointers=true  ObjectAlignmentInBytes=8
  -Xmx31g     MaxHeapSize=31744 MB  UseCompressedOops=true  UseCompressedClassPointers=true  ObjectAlignmentInBytes=8
  -Xmx31900m  MaxHeapSize=31904 MB  UseCompressedOops=true  UseCompressedClassPointers=true  ObjectAlignmentInBytes=8
  -Xmx32g     MaxHeapSize=32768 MB  UseCompressedOops=false  UseCompressedClassPointers=true  ObjectAlignmentInBytes=8
  -Xmx40g     MaxHeapSize=40960 MB  UseCompressedOops=false  UseCompressedClassPointers=true  ObjectAlignmentInBytes=8
  -Xmx40g   -XX:ObjectAlignmentInBytes=16: UseCompressedOops=true
  -Xmx60g   -XX:ObjectAlignmentInBytes=16: UseCompressedOops=true
  -Xmx64g   -XX:ObjectAlignmentInBytes=16: UseCompressedOops=false
  -Xmx70g   -XX:ObjectAlignmentInBytes=16: UseCompressedOops=false
  -Xmx4g -XX:-UseCompressedOops:UseCompressedOops=false,UseCompressedClassPointers=true

堆地址模式

实测 · 本机执行输出
  -Xmx1g   堆起始地址 0x0000000125800000,压缩指针模式「Zero based」,移位 3 位
  -Xmx4g   堆起始地址 0x0000007000800000,压缩指针模式「Non-zero disjoint base: 0x0000007000000000」,移位 3 位
  -Xmx16g  堆起始地址 0x0000007b70800000,压缩指针模式「Non-zero based: 0x0000007b70000000」,移位 3 位
  -Xmx31g  堆起始地址 0x0000007ff9000000,压缩指针模式「Non-zero based: 0x0000007ff8000000」,移位 3 位
  -Xmx33g  日志里没有 Compressed Oops 模式这一行(没有启用压缩指针)

对象大小

实测 · 本机执行输出
  [开启(默认)]
  new Object() 16 字节;空类 16;1 个引用字段 16;1 个 int 字段 16;Node(2 个引用 + 1 个 Object + 1 个 int)32
  new Object[0] 16 字节;new Object[1] 24;new Object[10] 56;new int[0] 16;new int[10] 56;new byte[1] 24
  new Integer(通过 Integer.valueOf(1000))16 字节;new Long 24;new String("") 对象本身 24
  [-XX:-UseCompressedOops]
  new Object() 16 字节;空类 16;1 个引用字段 24;1 个 int 字段 16;Node(2 个引用 + 1 个 Object + 1 个 int)40
  new Object[0] 16 字节;new Object[1] 24;new Object[10] 96;new int[0] 16;new int[10] 56;new byte[1] 24
  new Integer(通过 Integer.valueOf(1000))16 字节;new Long 24;new String("") 对象本身 32
  [-XX:-UseCompressedOops -XX:-UseCompressedClassPointers]
  new Object() 16 字节;空类 16;1 个引用字段 24;1 个 int 字段 24;Node(2 个引用 + 1 个 Object + 1 个 int)48
  new Object[0] 24 字节;new Object[1] 32;new Object[10] 104;new int[0] 24;new int[10] 64;new byte[1] 32
  new Integer(通过 Integer.valueOf(1000))24 字节;new Long 24;new String("") 对象本身 32

内存占用与遍历速度

实测 · 本机执行输出
  [开启(默认)]
  500 万个 Node(next/prev/data/v)+ 500 万元素的 Node[]:GC 之后堆占用 155 MB;建立耗时 120 ms;遍历链表一遍(15 次)中位 10.1 ms、最小 7.4 ms、最大 10.9 ms(校验和 187499962500000)
  [-XX:-UseCompressedOops]
  500 万个 Node(next/prev/data/v)+ 500 万元素的 Node[]:GC 之后堆占用 194 MB;建立耗时 221 ms;遍历链表一遍(15 次)中位 5.7 ms、最小 5.6 ms、最大 7.2 ms(校验和 187499962500000)
  [JDK 27 开启(默认)]
  500 万个 Node(next/prev/data/v)+ 500 万元素的 Node[]:GC 之后堆占用 117 MB;建立耗时 112 ms;遍历链表一遍(15 次)中位 9.4 ms、最小 7.1 ms、最大 10.5 ms(校验和 187499962500000)
  [JDK 27 -XX:-UseCompressedOops]
  500 万个 Node(next/prev/data/v)+ 500 万元素的 Node[]:GC 之后堆占用 194 MB;建立耗时 132 ms;遍历链表一遍(15 次)中位 5.7 ms、最小 5.6 ms、最大 7.3 ms(校验和 187499962500000)
  对照 JDK 27:UseCompressedOops=true,UseCompressedClassPointers=None,UseCompactObjectHeaders=true
  JDK 27 默认配置下的对象大小:
    new Object() 8 字节;空类 8;1 个引用字段 16;1 个 int 字段 16;Node(2 个引用 + 1 个 Object + 1 个 int)24
    new Object[0] 16 字节;new Object[1] 16;new Object[10] 56;new int[0] 16;new int[10] 56;new byte[1] 16
    new Integer(通过 Integer.valueOf(1000))16 字节;new Long 16;new String("") 对象本身 24
  JDK 27 加 -XX:+UseCompactObjectHeaders:
    new Object() 8 字节;空类 8;1 个引用字段 16;1 个 int 字段 16;Node(2 个引用 + 1 个 Object + 1 个 int)24
    new Object[0] 16 字节;new Object[1] 16;new Object[10] 56;new int[0] 16;new int[10] 56;new byte[1] 16
    new Integer(通过 Integer.valueOf(1000))16 字节;new Long 16;new String("") 对象本身 24

分步演示

Compressed Oops
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12 与 JDK 27,SerialGC,macOS Apple Silicon
    • 没有验证:Linux 上的 Unscaled/Zero based 模式、不同 GC 的差别、紧凑对象头在 JDK 27 之外版本中的状态、关闭压缩指针后遍历反而更快的原因(只有一台机器上的少量测量)、ZGC 下的压缩指针(ZGC 不使用)、为什么 -Xmx 31900m 附近就是边界(这篇测到的是开关结果,不是源码)
    京ICP备2026067502号-1京公网安备11010502063487号