Java Object Layout (JOL)
Java Object Layout (JOL)
官网:OpenJDK: jol
Github:GitHub - openjdk/jol: https://openjdk.org/projects/code-tools/jol
JOL (Java Object Layout) 是一个小型工具箱,用于分析 JVM 中的对象布局。这些工具大量使用 Unsafe、JVMTI 和 Serviceability Agent (SA) 来解码实际的对象布局、占用空间和引用。这使得 JOL 比依赖堆转储、规范假设等的其他工具要准确得多。
基本使用
官方文档:JOL使用手册
有两种方式使用 JOL,一种是作为一个依赖添加到 Maven 的 POM 文件中,一种是直接在命令行中使用
作为 Maven 依赖使用
注意,实践内容基于 java version "1.8.0_181",其他版本的 JDK 结果可能不一样
官方代码示例:JOL Samples。通过这些例子可以看到 JOL 工具的其他功能,以后有时间再去研究。
在模块的 POM 文件中添加依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
添加此以来之后,如果可能的话,JOL 模块将尝试自附加为 Java 代理(self-attach as Java Agent)。因此,建议将 Premain-Class
和 Launcher-Agent
属性添加到最终的 JAR 包的 META-INF/MANIFEST.MF
中。
具体做法请参考《Maven shade Plugin》的
生成可执行 jar 包
小节
<project>
......
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<!-- 替换为实际的执行入口 -->
<Main-Class>path.mainclass</Main-Class>
<Premain-Class>org.openjdk.jol.vm.InstrumentationSupport</Premain-Class>
<Launcher-Agent-Class>org.openjdk.jol.vm.InstrumentationSupport$Installer</Launcher-Agent-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
JOL 工具常用方法
-
查看对象内部信息:
ClassLayout.parseInstance(obj).toPrintable()
,对应查看对象内部布局
和作为命令行工具使用时的internals
小节 -
计算对象的大小(单位为字节):
ClassLayout.parseInstance(obj).instanceSize()
-
查看对象外部信息:包括引用的对象:
GraphLayout.parseInstance(obj).toPrintable()
,对应查看对象图
和作为命令行工具使用时的externals
小节 -
查看对象占用空间总大小:
GraphLayout.parseInstance(obj).totalSize()
查看对象内部布局
参考博客:JOL:Java 对象内存布局
新建一个 JOL 工程 JOL
,POM 文件为:
<project>
......
<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>xyz.xiashuo.JOLMain</Main-Class>
<Premain-Class>org.openjdk.jol.vm.InstrumentationSupport</Premain-Class>
<Launcher-Agent-Class>org.openjdk.jol.vm.InstrumentationSupport$Installer</Launcher-Agent-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
只有一个类 JOLMain
package xyz.xiashuo;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
/**
* @author xiashuo.xyz
* @date 2024/4/5 15:25
*/
public class JOLMain {
public static void main(String[] args) {
System.out.println("===================== VM DESC =====================");
System.out.println(VM.current().details());
System.out.println("===================== Java Object Layout =====================");
System.out.println(ClassLayout.parseInstance(new JOLMain()).toPrintable());
}
}
此时,我们可以直接在 IDE 中运行此 main 方法,不过为了模拟无法直接调试的情况,我们就还是选择运行 jar 包的方式来查看结果,放心,这两种方式输出的内容是一样的。
执行 mvn package
打包,打包结果是:
执行 java -jar .\JOL-1.0-SNAPSHOT.jar
,输出
===================== VM DESC =====================
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. Unable to attach even with module exceptions: [org.openjdk.jol.vm.sa.SASupportException: Sense failed., org.openjdk.jol.vm.sa.SASupportException: Sense failed., org.openjdk.jol.vm.sa.SASupportException: Sense failed.]
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
===================== Java Object Layout =====================
xyz.xiashuo.JOLMain object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c005
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
忽略告警信息,我们来一一解释
1、VM DESC:虚拟机相关信息
- Running 64-bit HotSpot VM:运行的虚拟机是 64 位 HotSpot 虚拟机
- Using compressed oop with 3-bit shift:说明开启了压缩指针
- Using compressed klass with 3-bit shift:说明开启了类信息压缩(TODO,不确定)
- Objects are 8 bytes aligned:对象是基于 8 byte 对齐,即每个对象的大小必须是 8 byte 的整倍数
- Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 和 Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]:描述了对象字段和数组元素各种数据类型占用的内存大小,依次是:
- 4 - 引用数据类型占用 4 byte(因默认开启压缩指针);
- 1 - boolean 占用 1 byte;
- 1 - byte 占用 1 byte;
- 2 - short 占用 2 byte;
- 2 - char 占用 2 byte;
- 4 - int 占用 4 byte;
- 4 - float 占用 4 byte;
- 8 - long 占用 8 byte;
- 8 - double 占用 8 byte
2、Java Object Layout:指定的对象在 Java 堆中保存为哪些比特,以及其结构
- 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0):对象的 Mark Word 占用了 8 byte(64 bit),此时存储的数据为:0x0000000000000001,即无锁状态
- 8 4 (object header: class) 0xf800c005:对象的 Class Pointer 占用了 4 byte(32 bit),因为开启了压缩指针,引用数据类型占用 4 byte
- 12 4 (object alignment gap):对象的 Padding 占用了 4 byte,因为 Mark Word + Class Pointer = 12 byte,不是 8 byte 的整数,所以需要 4 byte 的对齐填充为 8 字节的整倍数:16 字节
- Instance size: 16 bytes:当前对象占用的内存大小,当前对象没有实例数据,也就是一个对象最小占用内存大小为 16 byte(64 位虚拟机)
我们可以看到,在一个 64 位的虚拟机中,一个对象示例最小就占用 16byte 的空间。
上面我们提到,压缩指针默认是开启(-XX:+UseCompressedOops
)的,那当我们关闭压缩指针(-XX:-UseCompressedOops
),再运行一次 jar 包
如果是在 IDE 中直接运行 main 方法,可以编辑 main 方法对应的 Run/Debug Configurations,首先点击 Modify options,勾上 Add VM options,然后再 VM options 输入框中输入 -XX:-UseCompressedOops

如果是打成 jar 包来执行,我们需要执行 java -jar -XX:-UseCompressedOops .\JOL-1.0-SNAPSHOT.jar
,输出
===================== VM DESC =====================
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. Unable to attach even with module exceptions: [org.openjdk.jol.vm.sa.SASupportException: Sense failed., org.openjdk.jol.vm.sa.SASupportException: Sense failed., org.openjdk.jol.vm.sa.SASupportException: Sense failed.]
# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.
# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
===================== Java Object Layout =====================
xyz.xiashuo.JOLMain object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x00000000269c0430
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
我们只看有变化的部分:
1、VM DESC:虚拟机相关信息
- Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 和 Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]:关闭压缩指针后只有引用数据类型占用内存由 4 byte 变为 8 byte,其余基本数据类型占用内存不变
2、Java Object Layout:指定的对象在 Java 堆中保存为哪些比特,以及其结构
- 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0):较关闭压缩指针之前无变化
- 8 8 (object header: class) 0x00000000269c0430:Class Pointer 由之前占用 4 byte 变为占用 8 byte,由于此时 Mark Word + Class Pointer = 16 byte,是 8 byte 整数倍,所以不需要对齐填充
此时我们使用的是空对象,那么我们我们构造一个有字段的对象,再看看其再 Java 堆中的到底是什么,
修改 JOLMain
:
public class JOLMain {
public static void main(String[] args) {
System.out.println("===================== VM DESC =====================");
System.out.println(VM.current().details());
System.out.println("===================== Java Object Layout =====================");
// System.out.println(ClassLayout.parseInstance(new JOLMain()).toPrintable());
System.out.println(ClassLayout.parseInstance(new CustomObj()).toPrintable());
}
public static class CustomObj {
private String referenceField;
private boolean booleanField;
private byte byteField;
private short shortField;
private char charField;
private int intField;
private float floatField;
private long longField;
private double doubleField;
}
}
打包之后,执行 java -jar .\JOL-1.0-SNAPSHOT.jar
(不进行指针压缩,跟默认保持一致),输出
===================== VM DESC =====================
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. Unable to attach even with module exceptions: [org.openjdk.jol.vm.sa.SASupportException: Sense failed., org.openjdk.jol.vm.sa.SASupportException: Sense failed., org.openjdk.jol.vm.sa.SASupportException: Sense failed.]
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
===================== Java Object Layout =====================
xyz.xiashuo.JOLMain$CustomObj object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c869
12 4 int CustomObj.intField 0
16 8 long CustomObj.longField 0
24 8 double CustomObj.doubleField 0.0
32 4 float CustomObj.floatField 0.0
36 2 short CustomObj.shortField 0
38 2 char CustomObj.charField
40 1 boolean CustomObj.booleanField false
41 1 byte CustomObj.byteField 0
42 2 (alignment/padding gap)
44 4 java.lang.String CustomObj.referenceField null
Instance size: 48 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
首先我们可以看到整个对象,占用 48 字节,因此可以推断这个类的任意一个对象的大小都是 48 字节,因为只要类结构确定了,类对象的大小也就固定了,这样 Java 堆才能知道为其分配多大的空间。
此外我们可以清晰地看到每一种原始类型的字段的长度,比如 int 是 4 字节,long 是 8 字节,还可以看到在开启指针压缩的情况下,一个对象的引用(其实就是指针)所占用的空间是 4 字节。跟对象头中指向类信息的指针大小一样。对象引用(指针)的大小是固定的,具体的对象的内容保存在另外的内存空间中。
还记得刚学 Java 的时候,对原始数据类型的存储空间死记硬背,现在终于可以直观地看到每种数据类型地大小了。
此外我们还可以发现一个奇怪的地方,就是对其填充空间居然是放到引用类型的数据的前面的。
查看对象图
列出从当前实例可访问的对象,它们的地址,通过可达性图的路径等
其他的配置都跟 查看对象内部布局
小节一直,只是修改 JOLMain
类
public class JOLMain {
public static void main(String[] args) {
System.out.println("===================== Graph Layout =====================");
System.out.println(GraphLayout.parseInstance(new MyObj()).toPrintable());
}
public static class MyObj {
private String strObj = "123456789";
private Integer intObj = 12;
private Long longObj = 12452l;
private double doubleRaw = 4544d;
}
}
直接运行 main 方法,输出
===================== Graph Layout =====================
xyz.xiashuo.JOLMain2$MyObj@266474c2d object externals:
ADDRESS SIZE TYPE PATH VALUE
716c7b490 16 java.lang.Integer .intObj 12
716c7b4a0 891040 (something else) (somewhere else) (something else)
716d54d40 32 xyz.xiashuo.JOLMain2$MyObj (object)
716d54d60 24 java.lang.String .strObj (object)
716d54d78 40 [C .strObj.value [1, 2, 3, 4, 5, 6, 7, 8, 9]
716d54da0 384 (something else) (somewhere else) (something else)
716d54f20 24 java.lang.Long .longObj 12452
Addresses are stable after 1 tries.
通过这个结果我们可以看到特定对象的外部引用情况,address 就是引用指向的实际地址,size 就是被引用对象空间的大小,path 就是从当前对象到被引用对象的引用方式。
从这里我们可以看到 MyObj 对 Long 对象,Integer 对象,String 对象的引用。
作为命令行工具使用
JOL 也可以作为命令行工具使用,直接到 Maven Central 或者 here 下载即可。有的时候我们需要排查服务器端的 Java 堆内存问题,此时,JOL 命令行工具就能派上很大的用场。神器。
我这里下载的是 jol-cli-latest.jar
,本地 JDK 版本为 java version "1.8.0_181"
执行 java -jar jol-cli-latest.jar
输出
Usage: jol-cli.jar <operation> [optional arguments]*
Available operations:
externals: Show object externals: objects reachable from a given instance
footprint: Show the footprint of all objects reachable from a sample instance
heapdump-boxes: Read a heap dump and look for duplicate primitive boxes
heapdump-duplicates: Read a heap dump and look for probable duplicates
heapdump-estimates: Read a heap dump and estimate footprint in different VM modes
heapdump-stats: Read a heap dump and print simple statistics
heapdump-strings: Read a heap dump and look for duplicate Strings
internals: Show object internals: field layout, default contents, object header
internals-estimates: Same as 'internals', but simulate class layout in different VM modes
internals
这个命令会深入到对象布局: 对象内的字段布局,对象头信息,字段值,对齐/填充损失。
这个功能跟我们前面 作为Maven依赖使用
小节中展示的功能是一样的,例如通过 java -jar jol-cli-latest.jar internals java.util.HashMap
可以看到 HashMap 对象在 Java 堆中的信息,
但是不知道为什么,将 jol-cli-latest.jar
跟 JOLCli-1.0-SNAPSHOT.jar
放到同目录下,JOLCli-1.0-SNAPSHOT.jar
下有一个类 Main
package xyz.xiashuo;
/**
* @author xiashuo.xyz
* @date 2024/4/5 21:34
*/
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
然后执行 java -jar jol-cli-latest.jar internals xyz.xiashuo.Main
,却提示 xyz.xiashuo.Main
找不到,不知道为什么,TODO,算了,以后有时间再研究这个命令行工具的用法吧。
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. Unable to attach even with module exceptions: [org.openjdk.jol.vm.sa.SASupportException: Sense failed., org.openjdk.jol.vm.sa.SASupportException: Sense failed., org.openjdk.jol.vm.sa.SASupportException: Sense failed.]
# VM mode: 64 bits
# Compressed references (oops): 3-bit shift
# Compressed class pointers: 3-bit shift
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Object alignment: 8 bytes
# ref, bool, byte, char, shrt, int, flt, lng, dbl
# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8
# Array base offsets: 16, 16, 16, 16, 16, 16, 16, 16, 16
java.lang.ClassNotFoundException: xyz.xiashuo.Main
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Unknown Source)
at org.openjdk.jol.util.ClassUtils.loadClass(ClassUtils.java:70)
at org.openjdk.jol.operations.ClasspathedOperation.run(ClasspathedOperation.java:79)
at org.openjdk.jol.Main.main(Main.java:60)
internals-estimates
这就像 internals 一样,但是模拟了不同 VM 模式下的对象内存布局。这些工具将相似的布局组合在一起,并按对象实例大小降序排序。
externals
深入到对象图布局: 列出从当前实例可访问的对象,它们的地址,通过可达性图的路径等 (使用 API 更方便)。
footprint
获得对象空间占用估计(object footprint estimate),类似于对象外部,但以表格形式显示。
heapdump-stats - 非常有用
读取堆转储文件 .hprof
,并查看其高级统计信息。该工具单次在堆转储上运行,并且只占用少量额外内存。这允许在小型机器上处理巨大的堆转储。
查看统计信息
heapdump-estimates
读取堆转储并在不同的 VM 模式下投影内存占用。该工具单次在堆转储上运行,并且只占用少量额外内存。这允许在小型机器上处理巨大的堆转储。
探索压缩的引用是否会带来更大的对齐,像 Lilliput 这样的实验性特性是否会给工作负载带来好处,或者升级到更新的 JDK 或降级低比特的 JDK 是否有意义,这些都是很有用的。
heapdump-duplicates - 非常有用
读取堆转储并尝试识别具有相同内容的对象。如果可能的话,可以对这些对象进行重复数据删除。
它将打印每个类的摘要报告和更详细的报告。该工具单次在堆转储上运行,并占用一些内存来存储重复对象的哈希值。这允许在没有大量内存的情况下处理巨大的堆转储。如果堆转储不合适,则增加工具的堆大小。
可以检测重复对象,非常有价值
heapdump-boxes
类似于 heapdump-duplicates
,但侧重于原始数据类型的装箱。它更详细地说明了工作负载处理的原始数据类型装箱的范围,以及可能应用的重复数据删除/缓存策略。
该工具单次在堆转储上运行,并占用一些内存来存储重复框的值。这允许在没有大量内存的情况下处理巨大的堆转储。如果堆转储不合适,则增加工具的堆大小。
heapdump-strings - 非常有用
类似于 heapdump-duplicates
,但侧重于字符串。它更详细地说明了工作负载中有多少重复的字符串,以及可以应用哪些重复数据删除/缓存策略。
该工具分两次在堆转储上运行,并占用一些内存来存储重复字符串的值。这允许在没有大量内存的情况下处理巨大的堆转储。如果堆转储不合适,则增加工具的堆大小。
IDEA 插件
插件地址:JOL Java Object Layout - IntelliJ IDEs Plugin | Marketplace
看起来不咋样。