商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 如何通过 JVM 的 CompressedOops 技术在 32G 内存范围内有效节省对象引用的空间损耗

如何通过 JVM 的 CompressedOops 技术在 32G 内存范围内有效节省对象引用的空间损耗

  发布于2026-07-11 阅读(0)

扫一扫,手机访问

CompressedOops 在堆 ≤32GB 时默认自动开启,这不仅是 JVM 的底层内存布局策略,更不是什么“可选优化”;它真正生效的前提是堆大小落在 4GB 到 32GB 之间、没有显式禁用、对象按 8 字节对齐,并且系统能建立零基映射——否则就会静默退化回 64 位指针。

如何通过 JVM 的 CompressedOops 技术在 32G 内存范围内有效节省对象引用的空间损耗

在 32GB 堆以内,CompressedOops 默认生效,完全不需要手动开启——它不是什么“可选优化”,而是 JVM 在该区间自动启用的底层内存布局策略。

先说说一个常见的误区:很多人以为必须显式加上 -XX:+UseCompressedOops 才能启用。其实从 JDK 6u23 开始,这个选项就已经默认打开了。你真正需要操心的,反而是它为什么“没生效”。通常的原因不外乎这三种:

  • 堆上限 ≤4GB:JVM 直接退回到 32 位直接寻址,绕过了压缩逻辑——这其实是更优路径,不算失效。
  • 堆上限 >32GB:32 位指针无法覆盖 8 倍缩放后的地址空间,被迫回退到 64 位。
  • 手动设置了 -XX:-UseCompressedOops 或者把 -XX:ObjectAlignmentInBytes 改成了 16——后者破坏了末三位必须为 0 的条件,压缩自然失效。

为什么 -XX:+UseCompressedOops 在 ≤32GB 时几乎总在运行

64 位 JVM 启动时,只要 -Xmx 设在 4GB 到 32GB 之间(含边界),HotSpot 就会默认启用 CompressedOops,并且采用 ZeroBasedCompressedOOPS 模式:基地址为 0,指针只存偏移量,寻址公式简化为 (narrow-oop << 3) + base。那个左移 3 位的操作由 CPU 硬件指令(比如 LEA)高效完成,没有额外的函数调用开销。

很多人误以为必须手动加参数才能开启,实际上根本不用。真正让它“不工作”的情况,就是上面列出的那几种。换句话说,绝大多数场景下你什么都不用做,它就已经在默默帮你省内存了。

确认 CompressedOops 当前是否真正在工作

别光看启动参数,要验证运行时状态。最直接的方式有两条:

  • 执行 jstat -gc ,看输出里有没有 Compressed Oops 字样(JDK 8 及以上版本)。
  • 加上启动参数 -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode,JVM 启动时会打印类似这样的信息:
    heap address: 0x00000007c0000000, base: 0x0000000000000000, shift: 3

如果看到 zero basedshift: 3,说明正在用 ZeroBased 模式;如果显示 non-zero base,说明堆被碎片化或者内存布局受限,JVM 启用了带基地址的压缩模式——仍然有效,但多了一次加法。

32GB 边界附近容易踩的坑

设置 -Xmx32g 并不保证 CompressedOops 一定启用。实际能生效的上限略低于 32GB,典型值大约在 31.5~31.9GB 之间,具体取决于操作系统的虚拟内存布局、ASLR、JVM 内部保留区等因素。

  • 现象:启动时没报错,但 PrintCompressedOopsMode 显示 not compressed,或者 base: 0x... 后面跟了一个很大的数值。
  • 原因:JVM 在尝试分配连续虚拟地址空间时失败,被迫放弃零基压缩,改用带非零基地址的模式;如果连这也不行,就直接禁用压缩。
  • 对策:把 -Xmx 改成 -Xmx31g-Xmx30g,再验证一次。尽量避免卡在 32G 这个整数边界上。
  • 额外注意:-XX:CompressedClassSpaceSize 默认 1GB,如果加载了大量类(比如 Spring Boot 加多模块),可能会触发 ja va.lang.OutOfMemoryError: Compressed class space。这时候需要单独调大这个参数,但它不影响对象指针压缩。

对象引用节省的空间到底有多少

每个对象引用从 8 字节变成 4 字节,看似节省固定,但实际收益是放大的:

  • 一个 ArrayList 存 100 万个元素:引用数组本身从 8MB 降到 4MB;每个 String 还持有 char[] 引用,链式节省持续发生。
  • CPU 缓存行(64 字节)原来最多塞 8 个引用,现在可以塞 16 个;对象字段密集排列时,缓存命中率提升明显,尤其对遍历型操作(比如 stream.map)。
  • GC 扫描压力下降:CMS 或 G1 的并发标记阶段需要遍历所有引用字段,4 字节 vs 8 字节直接影响扫描吞吐量和 STW 时间。
  • 注意:压缩只作用于普通对象引用(ObjectString、数组元素等),不压缩基本类型、longdouble,也不压缩堆外内存(DirectByteBuffer.address)。

真正关键的不是“怎么开”,而是理解 JVM 在什么条件下自动关——尤其是堆大小正好落在 4GB 或 32GB 临界点附近时,行为会静默切换。生产环境上线之前,一定记得用 PrintCompressedOopsMode 实际验证,不要只依赖参数或者文档假设。

本文转载于:https://www.php.cn/faq/2380379.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注