如何获取Java对象真实地址?现有方法失效原因探究
问题背景
我想分析String对象在堆与常量池中的引用差异,计划通过jmap导出Java程序二进制文件来查找对象的二进制地址。尝试用System.out.println获取真实地址时,输出的是类似[Ljava.lang.String;@18b4aac2的内容,这并不是真实内存地址,没法用来匹配jmap导出的二进制数据。于是改用基于Unsafe类的代码,但结果仍不符合预期,想搞清楚:
- 如何正确获取String对象的真实内存地址?
- 为什么之前的方法得不到正确结果?
附上测试代码:
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

