You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何获取Java对象真实地址?现有方法失效原因探究

String对象堆与常量池引用分析:真实内存地址获取问题解决

问题背景

我想分析String对象在堆与常量池中的引用差异,计划通过jmap导出Java程序二进制文件来查找对象的二进制地址。尝试用System.out.println获取真实地址时,输出的是类似[Ljava.lang.String;@18b4aac2的内容,这并不是真实内存地址,没法用来匹配jmap导出的二进制数据。于是改用基于Unsafe类的代码,但结果仍不符合预期,想搞清楚:

  1. 如何正确获取String对象的真实内存地址?
  2. 为什么之前的方法得不到正确结果?

附上测试代码:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

        String[] key = {"qwerty"};
        String obj = "qwerty";
        Unsafe unsafe = getUnsafe();
        System.out.println(getAddressOf(key,unsafe));
        System.out.println(getAddressOf(key[0],unsafe));
        System.out.println(getAddressOf(args,unsafe));
        System.out.println(getAddressOf(args[0],unsafe));
        System.out.println(getAddressOf(obj,unsafe));
    }

    private static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }

    private static <T> String getAddressOf(T object, Unsafe unsafe) throws NoSuchFieldException {
        Object[] array = {object};
        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        int addressSize = unsafe.addressSize();
        long objectAddress;
        switch (addressSize) {
            case 4:
                objectAddress = unsafe.getInt(array, baseOffset);
                break;
            case 8:
                objectAddress = unsafe.getLong(array, baseOffset);
                break;
            default:
                throw new Error("unsupported address size: " + addressSize);
        }
        return Long.toHexString(objectAddress);
    }
}

问题原因解析

1. System.out.println输出的不是真实地址

@18b4aac2是对象哈希码的十六进制形式(默认由Object.hashCode()生成),和真实内存地址没有直接关联。哪怕部分JVM实现里哈希码初始值和内存地址挂钩,一旦GC触发对象移动,这个值就完全失效,根本没法对应jmap导出的堆文件地址。

2. 你的Unsafe代码存在的问题

你获取的是压缩后的对象引用:64位JVM默认开启指针压缩(-XX:+UseCompressedOops),此时对象引用会被压缩成32位值(去掉64位地址的低3位,因为对象对齐是8字节),所以你拿到的不是真实的64位内存地址。另外,字符串字面量(比如obj = "qwerty")存在于字符串常量池,你获取的地址是常量池项的地址,不是String对象本身的内存地址。

正确获取真实地址的方案

1. 临时关闭指针压缩(测试用)

启动JVM时添加参数-XX:-UseCompressedOops关闭指针压缩,此时你的Unsafe代码获取到的引用值就是真实的64位内存地址。但注意,这会增加内存开销,只适合测试场景。

2. 处理指针压缩转换

如果要在默认开启压缩的环境下获取真实地址,需要把压缩引用转换为真实地址,规则是真实地址 = 压缩引用值 << 3。修改后的getAddressOf方法如下:

private static <T> String getAddressOf(T object, Unsafe unsafe) throws NoSuchFieldException {
    Object[] array = {object};
    long baseOffset = unsafe.arrayBaseOffset(Object[].class);
    int addressSize = unsafe.addressSize();
    long realAddress;
    switch (addressSize) {
        case 4:
            // 压缩引用转真实地址
            long compressedAddr = unsafe.getInt(array, baseOffset);
            realAddress = compressedAddr << 3;
            break;
        case 8:
            // 关闭压缩时直接取长整型地址
            realAddress = unsafe.getLong(array, baseOffset);
            break;
        default:
            throw new Error("unsupported address size: " + addressSize);
    }
    return Long.toHexString(realAddress);
}

3. 区分常量池与堆中的String对象

  • 常量池中的String:字符串字面量存储在字符串常量池(JDK7及以后位于堆中),你获取的地址是常量池项的地址,不是String对象本身。要获取对象真实地址,需要通过Unsafe读取常量池项指向的对象地址。
  • 堆中的String:通过new String("qwerty")或字符串拼接生成的新对象,直接存在堆中,用上面的方法获取的地址就是对象真实内存地址,可以和jmap导出的堆文件对应。

4. 借助JVM工具直接查找(更简便)

不需要手动写代码获取地址的话,直接用jmap -dump:format=b,file=heap.bin <进程ID>导出堆快照,然后用JVisualVM或jhat heap.bin打开快照,直接查找String对象的内存地址,这种方式更准确,也避免了Unsafe的复杂操作。

关键注意点

  • JVM中的内存地址是虚拟地址,和jmap导出堆文件里的地址属于同一维度,不是物理内存地址。
  • GC会移动对象,所以获取的地址仅在当前GC周期内有效。测试时可以添加-XX:+DisableExplicitGC禁用显式GC,或者手动触发GC后立即获取地址。

内容的提问来源于stack exchange,提问作者fei HuFer hu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 17:35:09