
对象访问定位句柄与直接指针引言对象创建后Java 程序需要通过栈上的引用reference去操作堆中的对象。但引用到底如何定位到堆中的对象这个问题看似简单背后却涉及一个重要的设计决策reference 到 Java 对象的访问方式。不同的访问方式在 GC 稳定性和访问性能之间做出不同权衡。本篇将讲解两种主流的对象访问方式——句柄访问Handle和直接指针访问Direct Pointer分析它们各自的优劣以及主流 JVM 的实现选择。一、问题的起点reference 指向什么在 Java 代码中我们通过变量来操作对象ObjectobjnewObject();// obj 是栈上的 referenceobj.hashCode();// 通过 reference 访问堆中对象obj存在于 Java 栈帧的局部变量表中它是一个引用。但这个引用的内容是什么它存储的是堆中对象的直接地址还是一个间接的句柄地址这就是对象访问方式要回答的问题。------------------------------------------------------------------ | 栈帧局部变量表 Java 堆 | | ------------------ ----------------------- | | | obj (reference) | --- ? | Object 实例数据 | | | ------------------ ----------------------- | | | | | | 方法区 / 元空间 | | v | | ----------------------- | | | Object 类元数据 | | | ----------------------- | ------------------------------------------------------------------ | reference 到底指向哪里这就是访问方式要解决的问题 | ------------------------------------------------------------------主流的访问方式有两种句柄访问和直接指针访问。二、句柄访问Handle2.1 工作原理句柄访问方式下Java 堆中会划分出一块内存作为句柄池Handle Pool。reference 中存储的是对象的句柄地址而句柄中包含了对象实例数据与类型数据各自的地址。------------------------------------------------------------------ | 句柄访问方式 | ------------------------------------------------------------------ | | | 栈帧 Java 堆 方法区 | | | | ---------- ------------------ ---------- | | | | | 句柄池 | | | | | | ref | ----- | ------------ | | 类元数据 | | | | | | | 实例指针 |-----------| (Klass) | | | ---------- | | 类型指针 |--|--- | | | | | ------------ | | ---------- | | | | | | | | 其他句柄... | | | | --------|--------- | | | | | | | v | | | ------------------ | | | | 对象实例数据 |-- | | | (Instance Data) | | | ------------------ | ------------------------------------------------------------------ | 访问对象需要两次解引用ref → 句柄 → 实例数据/类型数据 | ------------------------------------------------------------------2.2 代码示意publicclassHandleAccessDemo{publicstaticvoidmain(String[]args){// 假设采用句柄访问PersonpnewPerson(Alice,25);// preference存储的是句柄地址而非对象直接地址// 访问流程// 1. 从栈中读取 p 的值 → 句柄地址 0x1000// 2. 访问句柄池 0x1000 → 获取实例指针 0x2000、类型指针 0x3000// 3. 通过实例指针 0x2000 访问堆中对象实例数据// 4. 通过类型指针 0x3000 访问方法区中 Person 类元数据p.getName();// 上述流程执行一次p.setAge(26);// 上述流程再执行一次}}2.3 句柄访问的优点GC 稳定性好这是句柄访问最大的优势。当 GC 移动对象时如复制算法、标记-整理算法只需要更新句柄中的实例指针而不需要修改所有指向该对象的 reference。------------------------------------------------------------------ | GC 移动对象时句柄方式只需更新句柄池 | ------------------------------------------------------------------ | | | GC 前 | | ref --- 句柄 [实例指针: 0x2000] --- 对象0x2000 | | | | GC 后对象被移动到 0x5000 | | | | 句柄方式 | | ref --- 句柄 [实例指针: 0x5000] --- 对象0x5000 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | 只需更新句柄池中的一处指针reference 不变 | | | | 直接指针方式 | | ref --- 对象0x5000 | | ^^^^^^^^^^^^^^^^^^^^^^^ | | 需要找到所有指向 0x2000 的 reference 并全部更新为 0x5000 | ------------------------------------------------------------------考虑一个场景堆中有 100 万个 reference 指向同一个对象。GC 移动该对象时句柄方式只需修改句柄池中 1 个指针直接指针方式需要修改 100 万个 reference2.4 句柄访问的缺点访问速度慢每次访问对象都需要两次指针解引用——先找到句柄再从句柄找到对象实例。在高频访问场景下这个开销不可忽视。额外内存开销句柄池本身占用堆内存。每个对象都需要一个句柄通常 8-16 字节对于对象数量庞大的应用句柄池的内存开销可观。三、直接指针访问Direct Pointer3.1 工作原理直接指针访问方式下reference 中存储的是对象在堆中的直接地址。对象的类型数据通过对象头中的Klass Pointer来定位。------------------------------------------------------------------ | 直接指针访问方式 | ------------------------------------------------------------------ | | | 栈帧 Java 堆 方法区 | | | | ---------- ------------------ ---------- | | | | | 对象头 | | | | | | ref | ----- | ------------ | Klass | 类元数据 | | | | | | | Mark Word | | Pointer | (Klass) | | | ---------- | ------------ |---------| | | | | | Klass Ptr | | ---------- | | | ------------ | | | | 实例数据 | | | | (Instance Data)| | | ------------------ | ------------------------------------------------------------------ | 访问对象只需一次解引用ref → 对象含对象头 实例数据 | ------------------------------------------------------------------3.2 代码示意publicclassDirectAccessDemo{publicstaticvoidmain(String[]args){// HotSpot 采用直接指针访问PersonpnewPerson(Alice,25);// preference存储的是对象的直接堆地址// 访问流程// 1. 从栈中读取 p 的值 → 对象地址 0x2000// 2. 直接访问堆中 0x2000 处的对象// - 对象头中包含 Klass Pointer → 方法区中的 Person 类元数据// - 紧跟对象头的是实例数据p.getName();// 一次解引用即可访问}}3.3 直接指针的优点访问速度快只需一次指针解引用就能定位到对象实例数据。对于频繁的对象访问省去了一次内存寻址的开销。由于对象访问是 Java 程序中最频繁的操作之一这个优化累积效果显著。无额外内存开销不需要维护独立的句柄池类型指针已包含在对象头中。3.4 直接指针的缺点GC 开销更大GC 移动对象时需要更新所有指向该对象的 reference。JVM 通过GC 根集合遍历和记忆集Remembered Set来找到需要更新的引用这增加了 GC 的复杂度和时间开销。不过现代 GC 技术如卡表、写屏障已经能高效处理引用更新问题直接指针的 GC 开销在实际中可控。四、两种方式对比对比维度句柄访问直接指针访问访问速度慢两次解引用快一次解引用GC 移动对象只更新句柄reference 不变需更新所有 reference内存开销句柄池额外开销无额外开销类型访问句柄中直接有类型指针通过对象头 Klass Pointer实现复杂度句柄池管理简单GC 引用更新复杂核心权衡句柄访问用空间换 GC 稳定性直接指针用 GC 复杂度换访问速度。------------------------------------------------------------------ | 权衡示意图 | ------------------------------------------------------------------ | | | 访问频率 ←--- 直接指针的优势场景高频访问 | | ^ | | | 速度 | | | ^ | | | | 直接指针 | | | | \ | | | | \ | | | | \ 交叉点 | | | | \ | | | | \ | | | | 句柄访问 | | | ----------------------- GC 移动频率 | | | 句柄的优势场景频繁 GC 移动 | | | | HotSpot 的选择访问频率远高于 GC 移动频率 → 直接指针 | ------------------------------------------------------------------五、各 JVM 实现的选择5.1 HotSpot JVMHotSpot 采用直接指针访问。这是基于以下考量访问速度优先Java 程序中对象访问极其频繁一次解引用的性能优势在累积后非常可观。对象头已含类型指针Klass Pointer 存储在对象头中无需额外的句柄中转。现代 GC 足够高效HotSpot 的 GC 通过写屏障和卡表机制能高效处理引用更新。// 可以通过 JOL 验证 HotSpot 使用直接指针importorg.openjdk.jol.info.ClassLayout;publicclassHotSpotAccessDemo{publicstaticvoidmain(String[]args){ObjectobjnewObject();// 打印对象地址System.out.println(ClassLayout.parseInstance(obj).toPrintable());// 对象头中 Klass Pointer 直接指向元空间中的类元数据// 证明 reference 直接指向对象而非句柄}}5.2 其他 JVM 实现JVM 实现访问方式说明HotSpot直接指针主流选择对象头含 Klass PointerOpenJ9 (Eclipse)句柄访问IBM 传统GC 稳定性优先Azul Zing直接指针低延迟 GC优化访问速度GraalVM直接指针基于 HotSpot保持一致Android ART直接指针移动端减少内存开销OpenJ9 的句柄选择IBM 的 OpenJ9前身 J9历史上采用句柄访问这与 IBM 在大型企业系统中的 GC 稳定性需求有关。但 OpenJ9 在较新版本中也引入了直接指针的优化路径采用混合策略。5.3 为什么 HotSpot 选择直接指针从统计数据看对象访问的频率远高于 GC 移动对象的频率------------------------------------------------------------------ | 频率对比示意 | ------------------------------------------------------------------ | | | 对象访问次数/秒 数百万 ~ 数千万 | | GC 移动对象次数/秒仅在 GC 期间发生通常每秒 1 次 | | | | 结论优化高频操作访问比优化低频操作GC移动更划算 | | | | HotSpot直接指针 → 每次访问省一次解引用 | | 代价GC 时多花一些时间更新引用可控 | ------------------------------------------------------------------HotSpot 团队认为在典型的 Java 应用中对象访问的频率比 GC 移动对象的频率高出几个数量级。因此优化访问速度直接指针比简化 GC 引用更新句柄更有价值。六、实际验证与思考6.1 从对象头验证直接指针importorg.openjdk.jol.info.ClassLayout;importorg.openjdk.jol.vm.VM;publicclassDirectPointerVerification{publicstaticvoidmain(String[]args){// 打印当前 JVM 的对象布局信息System.out.println(VM.current().details());StringstrHello;System.out.println(ClassLayout.parseInstance(str).toPrintable());// 输出中可以看到// 1. 对象头中有 Klass Pointer 字段// 2. reference 直接指向对象起始地址即对象头// 3. 没有句柄中间层}}6.2 GC 移动对象时的引用更新虽然直接指针方式下 GC 需要更新引用但 HotSpot 通过以下机制保证效率根集合枚举GC 从 GC Roots栈、全局引用等出发快速找到所有活跃引用。OopMapOop MapJVM 在安全点记录栈帧中引用的位置避免全栈扫描。写屏障Write Barrier维护跨代/跨 Region 引用关系避免全堆扫描。------------------------------------------------------------------ | HotSpot 直接指针 GC 引用更新机制 | ------------------------------------------------------------------ | | | GC Roots (栈、静态字段、JNI 引用等) | | | | | | OopMap 记录引用位置 | | v | | 枚举所有活跃引用 → 更新指向移动后对象的新地址 | | | | 跨代引用通过卡表 写屏障维护 | | ------------------------ | | | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 卡表每张卡 512B | | ------------------------ | | 正常 脏 正常 正常 脏 正常 正常 正常 | | ^ | | 只扫描脏卡对应的堆区域 | ------------------------------------------------------------------6.3 对开发者的启示了解访问方式对日常开发有实际意义// 场景高频缓存访问publicclassCacheAccessPattern{// 直接指针方式下cache.get(key) 只需一次解引用// 加上 HashMap 内部的链表/树遍历privatestaticfinalMapString,HeavyObjectcachenewHashMap();publicHeavyObjectget(Stringkey){// 访问链路// 1. 局部变量 key → 直接指向 String 对象// 2. cache.get() → HashMap 内部 Node 引用 → 直接指向 Node// 3. Node.value → 直接指向 HeavyObject// 全程直接指针无句柄中转returncache.get(key);}}在直接指针模型下减少间接层级如避免不必要的包装类、减少链式引用对性能有直接帮助。实践要点HotSpot 默认直接指针无需配置开发者通常无需关心访问方式HotSpot 已做出最优选择。但理解这一机制有助于在性能分析时做出正确判断。GC 移动对象不影响引用正确性无论 GC 如何移动对象reference 始终指向正确的对象HotSpot 在 GC 时更新所有引用。不要在 JNI 代码中缓存对象指针跨 GC 使用应使用GetLocalRef/NewGlobalRef等 JNI 引用管理 API。安全点与引用更新GC 更新引用需要安全点Safepoint这是 Stop-The-World 的原因之一。减少对象数量可以缩短安全点同步时间。压缩指针与访问效率开启指针压缩-XX:UseCompressedOops不仅节省内存还减少了指针的宽度对 CPU 缓存行利用率有正面影响。在 32GB 以下堆环境务必保持开启。跨 JVM 迁移注意从 OpenJ9 迁移到 HotSpot或反向时对象访问方式的差异可能导致相同的代码表现出不同的内存占用和 GC 行为。在做性能基准对比时需考虑这一因素。小结对象访问有句柄访问和直接指针访问两种方式核心区别在于 reference 指向的是句柄还是对象本身。句柄访问reference → 句柄 → 对象GC 移动对象时只需更新句柄但每次访问多一次解引用。直接指针访问reference → 对象访问速度最快GC 移动对象时需更新所有引用。HotSpot 采用直接指针因为对象访问频率远高于 GC 移动频率优化访问速度收益更大。对象头中的Klass Pointer使得直接指针方式也能高效访问类型数据无需额外中转。现代 GC 的OopMap、卡表、写屏障机制使直接指针的引用更新开销可控。下一篇我们将深入 Java 中最特殊的对象——String探讨 String 常量池的位置变迁与intern()方法的工作原理。更多内容JVM调优实战