⭐ 推荐:社区规则条款 V1.0

如何用 JOL 分析 Java 对象的内存布局?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-24 02:06 ·6 浏览 ·0 回复
内容摘要

介绍使用 JOL 0.17 分析 Java 对象内存布局的流程:添加依赖并确认 JVM 指针与对齐参数,再用 ClassLayout 打印对象头、字段偏移、对齐填充及压缩指针占用,同时提示需在 HotSpot 上运行并为 JDK 9+ 配置

学完这篇,你能独立用 JOL 把一个 Java 对象的内存布局逐字节打印出来,看懂对象头、字段偏移、对齐填充和压缩指针各自占了多少字节。

JOL 全称 Java Object Layout,是 OpenJDK 官方维护的小工具包,作用是绕过「凭感觉估算」,直接把 JVM 里对象的真实内存排布打出来。下面按从零开始到能排查问题的顺序走一遍。

第一步:添加依赖,检查 JOL 能否正常运行

这一步的目标是让项目能编译运行 JOL 的两行核心 API,建议选用 0.17 版。Maven 项目在 pom.xml 的 <dependencies> 里加:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

Gradle 项目加 implementation 'org.openjdk.jol:jol-core:0.17'。JOL 0.17 支持 JDK 8 及以上,不需要额外装什么。

注意:JOL 的底层假设基于 HotSpot 系列 JVM(OpenJDK、Oracle JDK、阿里 Dragonwell 等)。如果你跑在 OpenJ9 上,输出结果会不准,先换回 HotSpot 再分析。

第二步:先打印当前 JVM 的内存前提

这一步能得到一份「当前 JVM 用多少位指针、对象按几字节对齐」的参数说明,也是后面看输出时判断是否正确的基准。新建一个 Demo 类:

import org.openjdk.jol.vm.VM;

public class Demo {
    public static void main(String[] args) {
        System.out.println(VM.current().details());
    }
}

运行后你会看到类似 # Running 64-bit HotSpot VM.、# Objects are 8 bytes aligned.、# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 这样的几行。第三行是引用、boolean、byte、char、short、int、float、long、double 的字节数,记住第一个数字是引用大小——4 字节说明压缩指针(CompressedOops)是开着的。

注意:JDK 9 以后如果看到 could not attach 之类的警告,加 JVM 参数 -Djdk.attach.allowAttachSelf=true 再跑;否则 JOL 拿不到某些底层信息。

第三步:用 ClassLayout 打印单个对象的字段布局

这一步能得到一个对象从偏移 0 开始的完整字节分布图,包含对象头、每个字段的起始偏移和末尾的填充字节。定义一个测试类并打印:

import org.openjdk.jol.info.ClassLayout;

public class Demo {
    static class Order {
        int id;
        long createTime;
        boolean paid;
        Object ext;
    }

    public static void main(String[] args) {
        Order o = new Order();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

输出大致是:

com.example.Demo$Order object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4                    (object header: class)    0x000010a8
 12   4                int Order.id                  0
 16   8               long Order.createTime          0
 24   1            boolean Order.paid                false
 25   3                    (alignment/padding gap)
 28   4             Object Order.ext                 null
Instance size: 32 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

如果只想分析类结构、不创建实例,用 ClassLayout.parseClass(Order.class).toPrintable()。

第四步:解读输出

这一步要把上面那几行读明白。

  • OFF 0,8 字节是 mark word,存哈希码、GC 年龄、锁状态标志。
  • OFF 8 那 4 字节是类型指针(klass pointer),开启压缩指针后是 4 字节,否则 8 字节。
  • Order.id 从偏移 12 开始——而不是从 16,说明 JVM 会把窄字段往前塞。
  • 字段顺序并不是源码里写的顺序。HotSpot 默认按「long/double → int/float → short/char → byte/boolean → 引用」从宽到窄排列,目的是减少填充。所以 long createTime 排到了 int id 后面。
  • alignment/padding gap 3 字节和老末尾凑齐 8 的倍数,这是对象对齐规则:对象总大小必须是 8 的整数倍(ObjectAlignmentInBytes 默认 8)。这个例子里实例大小 28 字节本身不是 8 的倍数,实际会补到 32。

注意:toPrintable() 打印的是当前这个对象实例,但字段偏移是类级别的固定值,同一个类的所有实例布局一致,只有对象头里的 mark word 取值不同。

第五步:对比压缩指针开启和关闭的差异

这一步能验证「为什么同样一个类,在不同 JVM 参数下占的内存不一样」。在 IDEA 的 Run Configuration → Modify options → Add VM options 里,分别填写:

-XX:+UseCompressedOops -XX:+UseCompressedClassPointers

和

-XX:-UseCompressedOops -XX:-UseCompressedClassPointers

各跑一次第三步的代码。前者的 klass pointer 是 4 字节、引用字段 4 字节;后者两者都变 8 字节,Order 的实例大小会明显变大。这也解释了一个常见困惑:堆内存超过 32GB 后压缩指针自动失效,对象突然「变胖」了。

注意:-XX:+UseCompressedOops 在堆小于约 32GB 时默认开启,但某些 JVM 参数组合(比如特定 GC 或堆过大)会关掉它,别假设它一定开着,用第二步的 details() 确认。

第六步:用 GraphLayout 看整个对象图

这一步解决「一个对象引用了别的对象,总共占多少内存」。把 ClassLayout.parseInstance 换成:

System.out.println(GraphLayout.parseInstance(o).toFootprint());

toFootprint() 会按类汇总:这个类有多少个实例、每个多大、合计多少字节。排查缓存里一个 HashMap 到底吃多少内存时,这是最快的办法。

注意:GraphLayout 会遍历对象图,遇到超大集合(几十万条)会很慢甚至撑爆内存,排查线上问题时先在测试环境用相同结构的小数据跑通再上。

第七步:不写代码也能查——用 jol-cli 命令行

这一步适用于「只想看看 JDK 自带类长什么样,不想建工程」。去 Maven Central 下载 jol-cli-0.17-full.jar,然后执行:

java -jar jol-cli-0.17-full.jar internals java.util.HashMap
java -jar jol-cli-0.17-full.jar externals java.lang.String
java -jar jol-cli-0.17-full.jar footprint java.util.HashMap

internals 看字段布局,externals 看对象引用的外部对象,footprint 看整体占用。加 -p 可以传 JVM 参数,比如 java -jar jol-cli-0.17-full.jar internals -p -XX:-UseCompressedOops java.util.HashMap。

小结

  • JOL 的核心就三个 API:VM.current().details() 看 JVM 前提,ClassLayout 看单对象字段布局,GraphLayout 看对象图总占用。
  • 对象大小 = 对象头(mark word 8 字节 + klass pointer 4 或 8 字节)+ 字段 + 对齐填充,总大小必须是 8 的倍数。
  • 字段会被 JVM 重排序,从宽到窄排,别按源码顺序算偏移。
  • 压缩指针开关直接决定引用是 4 字节还是 8 字节,分析前先用 details() 确认当前状态。
  • JOL 只对 HotSpot 系 JVM 准确;JDK 9+ 建议加 -Djdk.attach.allowAttachSelf=true。
  • 不想建工程就用 jol-cli 的 internals / externals / footprint 三个子命令。
本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2438.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~