
类型指针与Klass——对象是如何找到自己的类的前言在前两篇文章中我们深入探讨了MinorGC的完整流程和动态年龄判定机制。有读者在评论区问了一个非常深刻的问题“我们知道对象是通过类型指针找到自己的Klass但类型指针是如何找到Klass的它是一个逻辑概念还是物理存在的”这个问题看似简单实则触及了JVM内部实现的核心——OOP-Klass模型。今天我们就来彻底揭开类型指针的神秘面纱看看Java对象在JVM内部到底长什么样子。一、从一道经典面试题说起在Java面试中经常有这样的问题Stringshello;Class?clazzs.getClass();// 这个getClass()是怎么实现的很多人能回答“通过对象头的类型指针找到方法区的类元数据”。但如果面试官追问“类型指针是什么它长什么样怎么找到类元数据”很多人就答不上来了。要回答这个问题我们需要深入HotSpot VM的源码看看对象在内存中的真实模样。二、OOP-Klass模型JVM内部的双胞胎HotSpot虚拟机在设计时采用了OOP-Klass模型来描述Java对象和类。这是两个层面的C数据结构2.1 OOPOrdinary Object Pointer描述对象实例OOP是指向对象实例的普通指针。每个Java对象在JVM内部都被表示为一个oopDesc对象存储在堆中。// HotSpot源码中的oopDesc结构简化版classoopDesc{private:volatilemarkOop _mark;// Mark Word存储哈希码、GC年龄、锁状态等Klass*_klass;// 类型指针指向方法区中的Klass对象// ... 其他字段};关键发现所谓的类型指针就是oopDesc结构中的_klass字段它是一个C指针存储的是内存地址。2.2 Klass描述类的元数据Klass是JVM内部用来表示Java类的C对象存储在方法区元空间。每个Java类在JVM内部对应一个InstanceKlass对象。// HotSpot源码中的InstanceKlass结构简化版classInstanceKlass:publicKlass{private:ConstantPool*_constants;// 常量池ArrayMethod**_methods;// 方法表包含所有方法入口地址ArrayFieldInfo*_fields;// 字段信息Klass*_super;// 父类Klass指针// ... 其他元数据};三、类型指针的本质一个内存地址现在我们可以回答核心问题了类型指针是一个物理存在的内存地址它直接存储了方法区中Klass对象的起始地址。当JVM在堆中创建一个对象时会在对象头中写入一个指针值这个值就是方法区中对应Klass的内存地址。当程序需要通过对象获取类信息时JVM直接读取这个地址跳转到方法区的对应位置。3.1 内存布局示意图堆中的对象实例 ┌──────────────────────────────┐ │ 对象头 │ │ ├─ Mark Word │ │ └─ 类型指针 0x7f6488c00730 ──┐ ← 这个就是内存地址 └──────────────────────────────┘ │ ↓ 方法区元空间 ┌──────────────────────────────┐ │ InstanceKlass结构体 │ │ 地址0x7f6488c00730 │ │ ├─ 类名java.lang.String │ │ ├─ 方法表 │ │ ├─ 字段信息 │ │ └─ ... │ └──────────────────────────────┘3.2 用数字说话类型指针的真实值通过HSDBHotSpot Debugger工具我们可以实际打印出对象头中的类型指针对象地址0xa3cf4878 对象头内容 _mark: 0x000000001a3c4e01 (哈希码、GC年龄、锁状态) _klass: 0x00007f6488c00730 ← 这就是类型指针的值这个0x00007f6488c00730就是一个真实的内存地址指向方法区中的Klass对象。四、类型指针的工作机制一个方法的完整调用过程理解了类型指针的本质我们来看一个完整的方法调用过程Stringshello;intlens.length();// 调用String的length()方法JVM执行s.length()时的底层操作Step 1从栈帧中找到对象引用栈帧虚拟机栈 ┌─────────────────────┐ │ 局部变量表 │ │ ├─ s: 0xa3cf4878 ────┐ ← s是引用存储对象地址 └─────────────────────┘ │ ↓ 堆内存 ┌─────────────────────┐ │ String对象实例 │ │ 地址0xa3cf4878 │ │ ├─ Mark Word │ │ └─ _klass: 0x7f6488c00730 └─────────────────────┘Step 2通过类型指针找到Klass堆中的对象 ┌─────────────────────┐ │ _klass: 0x7f6488c00730 ───┐ └─────────────────────┘ │ ↓ 方法区 ┌─────────────────────┐ │ InstanceKlass │ │ 地址0x7f6488c00730│ │ ├─ 类名String │ │ ├─ 方法表 │ │ │ ├─ length() ────────┐ │ │ ├─ charAt() │ │ │ └─ ... │ │ └─ 字段信息 │ └─────────────────────┘Step 3从方法表中找到目标方法方法表中的length()方法 ┌─────────────────────┐ │ 方法入口地址0x7f6488a1c200 ───┐ └─────────────────────┘ │ ↓ 代码段 ┌─────────────────────┐ │ length()方法的字节码 │ │ 地址0x7f6488a1c200 │ │ iconst_0 │ │ arraylength │ │ ireturn │ └─────────────────────┘Step 4跳转到方法入口执行JVM最终跳转到0x7f6488a1c200执行length()方法的字节码。整个过程的关键每一步都是通过内存地址直接跳转没有任何查找的开销。这就是类型指针作为内存地址的高效之处。五、验证用HSDB亲眼看到类型指针如果你想亲眼看到类型指针的真实面目可以用JDK自带的HSDB工具。5.1 准备测试代码publicclassTypePointerTest{privateinta10;privateStringstrhello;publicstaticvoidmain(String[]args)throwsException{TypePointerTestobjnewTypePointerTest();System.out.println(对象创建完成请用HSDB连接);System.in.read();// 暂停等待HSDB连接}}5.2 使用HSDB观察# 找到Java进程IDjps# 启动HSDBjava-cp$JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB在HSDB中点击Tools → Object Histogram找到TypePointerTest实例双击对象地址打开Inspector窗口在Inspector中可以看到_mark: Mark Word的值_klass: 指向InstanceKlass的指针点击_klass的值跳转到方法区中的Klass对象在Klass对象中可以看到常量池、方法表等元数据六、常见问题解答Q1每个对象都有类型指针那类型指针占用多少内存答在64位JVM中如果不开启指针压缩类型指针占用8字节。开启指针压缩-XX:UseCompressedOops后压缩为4字节。Q2指针压缩是怎么工作的答JVM知道所有对象都按8字节对齐所以对象地址的后3位总是0。因此可以存储压缩后的值右移3位使用时再左移3位还原。例如真实地址0x7f6488c00730二进制最后3位是000压缩存储0x7f6488c00730 3 0x0fec911800e6使用时0x0fec911800e6 3 0x7f6488c00730Q3Class对象和Klass是什么关系答Klass方法区中的C对象JVM内部使用存储类的完整元数据Class对象堆中的Java对象供Java代码使用String.class、obj.getClass()关系Class对象中也有一个类型指针指向同一个Klass堆中的Class对象 ┌─────────────────┐ │ Class对象 │ │ ├─ Mark Word │ │ └─ _klass ───────┐ └─────────────────┘│ ↓ 方法区 ┌─────────────────┐ │ String的Klass │ └─────────────────┘ ↑ 堆中的String实例 ┌─────────────────┐ │ String对象 │ │ ├─ Mark Word │ │ └─ _klass ───────┘ └─────────────────┘Q4类型指针是在什么时候写入对象头的答在对象创建时。当执行new指令时JVM完成内存分配后会立即设置对象头设置_mark初始化哈希码、GC年龄0、锁状态设置_klass指向方法区中对应类的Klass对象Q5如果对象被移动GC类型指针会变吗答不会。GC移动对象时会复制整个对象包括对象头到新位置类型指针的值保持不变——因为Klass在方法区中不会移动。这就是类型指针的稳定性。七、从类型指针看JVM的设计哲学理解类型指针的物理实现后我们能感受到JVM设计的精妙7.1 空间换时间每个对象都存储一个指针4/8字节指向Klass换来了对象-类信息的O(1)访问速度7.2 解耦对象与类对象在堆中Klass在方法区对象可以移动GC但不影响类信息的访问7.3 为多态提供基础ObjectobjnewString();obj.toString();// 调用String的toString()JVM通过obj的类型指针找到String的Klass从Klass的方法表中找到toString()的入口地址实现动态绑定7.4 类型检查的高效实现if(objinstanceofString){// ...}JVM通过obj的类型指针找到Klass检查这个Klass是否是String或String的子类整个过程是内存地址的比较非常快八、总结通过本文的深入剖析我们得出结论类型指针的本质是一个物理存在的内存地址存储在对象头的_klass字段中OOP-Klass模型OOPoopDesc堆中的对象实例包含_mark和_klassKlassInstanceKlass方法区中的类元数据包含方法表、常量池等工作流程对象通过_klass指针找到方法区中的Klass从Klass的方法表中找到目标方法入口跳转到方法入口执行设计精妙内存地址直接访问无查找开销对象可移动类信息固定为多态、类型检查提供基础理解类型指针的物理实现让我们真正看清了Java对象的内存布局也更能体会JVM设计者的匠心。这正是从会用到懂原理的必经之路。参考资料《深入理解Java虚拟机》周志明HotSpot VM源码openjdk/jdk/src/hotspot/share/oops/Oracle官方文档Java Virtual Machine Specification如果你觉得本文有帮助欢迎点赞、评论、转发你的支持是我持续输出的动力。