JVM探索-4.对象头

时间:2022-10-29 23:43     作者:林立     分类: 作品


  C++语言本身支持多态调用,众所周知,C++完成多态依赖于虚指针,这个指针指向一个虚表,这个虚表里面存储的是虚函数的地址,而这些函数的地址是在C++代码编译时确定的,通常虚表位于程序的数据段中。
  因为Java代码首先被翻译成字节码(bytecode),在JVM执行时才能确定要执行函数的地址,如何实现Java的多态调用,最直观的想法是把Java对象映射成C++对象或者封装成C++对象,比如增加一个额外的对象头,里面指向一个对象,而这个对象存储了Java代码的地址。所以JVM设计了对象的数据结构来描述Java对象,这个结构分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。而刚才提到的类似虚指针的东西就可以放在对象头中,而JVM设计者还利用对象头来描述更多信息,对象的锁信息、GC标记信息等。
  JVM中对象头分为两部分:标记信息、元数据信息,代码如下所示:

hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  // 静态变量用于快速访问BarrierSet
  static BarrierSet* _bs;
……
}

1.标记信息

  第一部分标记信息位于MarkOop。
  根据JVM源码的注释,针对标记信息在32位JVM用32位来描述,可以总结出这32位的组合情况。

  另外在源代码中我们还看到一个Promoted的状态,Promoted指的是对象从新生代晋升到老生代时,正常的情况需要对这个对象头进行保存,主要的原因是如果发生晋升失败,需要重新恢复对象头。如果晋升成功这个保存的对象头就没有意义。所以为了提高晋升失败时对象头的恢复效率,设计了promo_bits,这个其实是重用了加锁位(包括偏向锁),实际上只需要在以下三种情况时才需要保存对象头:
   - 使用了偏向锁,并且偏向锁被设置了。
   - 对象被加锁了。
   - 对象设置了hash_code。
  这里和GC直接相关的就是标记位11,前面的30位指针是非常有用的。在GC垃圾回收时,当对象被设置为marked(11)时,ptr指向什么位置?简单来说这个ptr是为了配合对象晋升时发生的对象复制(copy)。在对象复制时,先分配空间,再把原来对象的所有数据都复制过去,再修改对象引用的指针,就完成了。但是我们要思考这样一个问题,当有多个引用对象的字段指向同一个被引用对象时,我们完成一个被引用对象的复制之后,其他引用对象还没有被遍历(即还指向被引用对象老的地址),如何处理这种情况?这个时候简单设置状态为marked,表示被引用对象已经被标记且被复制了,ptr就是指向新的复制的地址。当遍历其他引用对象的时候,发现被引用对象已经完成标记,则不再需要复制对象,直接完成对象引用更新就可以了。

2.元数据信息

  第二部分元数据信息字段指向的是Klass对象(Klass对象是元数据对象,如Instance Klass描述Java对象的类结构),这个字段也和垃圾回收有关系。
  就是在垃圾回收的时候如何区别一个立即数和指针地址?比如从Java的根集合中发现有一个值(如:0X12345678),那么这个数到底是一个整数还是一个Java对象的地址?实际上垃圾回收器不能区别,但是为了准确地回收垃圾,必须区别出来。一个简单的办法就是,把0X12345678先看成一个地址,即强制转换成OOP结构,再判定这个OOP是否是含有Klass指针,如果有的话即认为是一个指针,如果是NULL的话则认为是一个立即数。那么这里会有一个误判,即把一个立即数识别成一个OOP,当这个立即数刚好和一个OOP的地址相同的时候。所以JVM维护了一个全局的OOpMap,用于标记栈里面的数是立即数还是值。每一个InstanceKlass都维护了一个Map(OopMapBlock)用于标记Java类里面的字段到底是OOP还是int这样的立即数类型。这里面的字段Klass很多时候用于再次确认。
  由此可见,可以从根集合出发开始标记,通过外部的数据结构来标识是否为OOP对象。但是我们在JVM源码中还是看到了很多地方会根据对象头里面的Klass指针是否为NULL来判断是不是OOP对象,这似乎是多此一举。理论上根据额外的数据结构已经不需要再次判断,但是在垃圾回收的时候,通常是对整个区域的一块内存进行完全遍历,在对象分配时都是连续分配,当堆的尾部有尚未分配对象的时候,比如在新生代一个字通常初始化为0x20202020,需要对这些空白地址进行转换以判断是否为OOP,是否需要垃圾回收。在这里即使误判影响也不大,因为会根据RSet来判定是否为活跃对象(live object),如果是的话继续,即使误判之后也没关系,这相当于是浮动垃圾,在下一次回收的时候仍然可能被回收。