如何用 JOL 分析 Java 对象的内存布局?
介绍使用 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 gap3 字节和老末尾凑齐 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三个子命令。
转载请注明出处,版权归原作者所有。