彩蛋:用HSDB工具偷看对象内存布局 - 当Java对象开始裸奔

192 阅读4分钟

《彩蛋:用HSDB工具偷看对象内存布局 - 当Java对象开始裸奔》


前置准备

# 需要JDK8环境(JDK9+默认禁用SA工具)
# 添加JVM启动参数允许调试
java -XX:+UnlockDiagnosticVMOptions -cp YourClassPath YourMainClass
  • -XX:+UnlockDiagnosticVMOptions

    • 这是一个JVM选项,用于解锁诊断工具相关的功能。
    • 启用后,可以使用其他诊断工具(如jstackjmap等)来分析正在运行的Java进程。
  • -cp YourClassPath

    • -cpclasspath 的缩写,用于指定应用程序所需的类路径。
    • YourClassPath 应替换为实际的类路径(例如:./out/production:libs/*)。
  • YourMainClass

    • 这是你的Java应用程序的主类(即包含 public static void main(String[] args) 方法的类)。
    • 替换为实际的主类名称。

实战代码

public class ObjectMemoryPeek {
    // 故意不对齐的字段布局
    private int id = 42;
    private boolean flag = true;
    private String name = "HSDB侦探";

    public static void main(String[] args) throws Exception {
        ObjectMemoryPeek target = new ObjectMemoryPeek();
        System.out.println("目标对象已创建,对象地址:" + target.hashCode());
        System.in.read(); // 在此处阻塞,等待HSDB附加
    }
}

侦探行动指南

第一步:启动HSDB(打开你的X光机)

# Windows
jdk8\bin\java.exe -cp jdk8\lib\sa-jdi.jar sun.jvm.hotspot.HSDB

# Linux/Mac
sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

操作图解:

  1. 点击菜单栏 File -> Attach to HotSpot process
  2. 输入刚刚运行的Java进程PID(可用jps命令查看)
  3. 恭喜你!现在能看到JVM的裸奔现场了

第二步:定位对象地址(给对象装上GPS)

// 代码中打印的hashCode并非真实地址,需要转换
// 在HSDB控制台执行:
hsdb> revptrs 0x00000000e5c9f158 // 替换为你的hashCode

技术原理:

对象头包含 mark word(存储哈希码和锁状态)和 klass pointer(指向类元数据),类似身份证信息


第三步:解剖对象内存(法医现场)

# 在HSDB控制台输入(假设对象地址是0x7f0a7230)
hsdb> inspect 0x7f0a7230

你会看到这样的裸奔数据:

// 对象头(12字节)
0x7f0a7230: 0x0000000000000001 (mark word)
0x7f0a7238: 0x0000000800c01280 (klass pointer)

// 实例数据
0x7f0a7240: 0x0000002a        // int id = 42
0x7f0a7244: 0x01              // boolean flag = true
0x7f0a7248: 0x00000007f0a7230 // String name引用地址

// 对齐填充(为了8字节对齐)
0x7f0a7250: 0x0000000000000000 

幽默解读:

  • 对象头就像对象的身份证(记录婚姻状态:是否被synchronized锁占有)
  • 对齐填充是JVM的强迫症表现(必须凑够8的倍数字节)

技术深挖

对象内存布局公式

对象内存 = 对象头(12B) + 实例数据 + 对齐填充

对象头详解:

组成部分大小内容说明
Mark Word8B哈希码、GC年龄、锁状态标志等
Klass Pointer4B类型指针(开启压缩指针时)

内存地址验证实验

// 在HSDB中执行:
hsdb> mem 0x7f0a7240 3
// 输出应显示:
0x7f0a7240: 0x0000002a  // id=42
0x7f0a7244: 0x00000001  // flag=true(实际存储为1)
0x7f0a7248: 0x7f0a7230  // name引用地址

防翻车指南

  1. 指针压缩陷阱
    如果看到0x0000000800c01280这样的klass指针,说明开启了压缩指针(UseCompressedOops)

  2. 大小端模式
    内存数据显示为小端模式(低位在前),比如int值42显示为0x2a000000

  3. 对象存活验证
    在HSDB中使用scanoops命令扫描堆内存,确保对象未被GC回收


彩蛋中的彩蛋:
试试在HSDB中执行universe命令,你会看到整个堆内存的上帝视角!