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-ClassLauncher-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 工具常用方法

查看对象内部布局

参考博客: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:虚拟机相关信息

2、Java Object Layout:指定的对象在 Java 堆中保存为哪些比特,以及其结构

我们可以看到,在一个 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:虚拟机相关信息

2、Java Object Layout:指定的对象在 Java 堆中保存为哪些比特,以及其结构

此时我们使用的是空对象,那么我们我们构造一个有字段的对象,再看看其再 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.jarJOLCli-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

看起来不咋样。