《JVM G1源码分析和调优》 —2.4 对象头
2.4 对象头
我们都知道Java语言是多态,那么如何实现多态?C++语言本身支持多态调用,众所周知,C++完成多态依赖于一个指针:虚指针(virtual pointer),这个指针指向一个虚表(virtual table),这个虚表里面存储的是虚函数的地址,而这些函数的地址是在C++代码编译时确定的,通常虚表位于程序的数据段(Data Segment)中。
因为Java代码首先被翻译成字节码(bytecode),在JVM执行时才能确定要执行函数的地址,如何实现Java的多态调用,最直观的想法是把Java对象映射成C++对象或者封装成C++对象,比如增加一个额外的对象头,里面指向一个对象,而这个对象存储了Java代码的地址。所以JVM设计了对象的数据结构来描述Java对象,这个结构分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。而我们刚才提到的类似虚指针的东西就可以放在对象头中,而JVM设计者还利用对象头来描述更多信息,对象的锁信息、GC标记信息等。我们这里只讨论和G1相关的信息,更多信息大家可以参考其他书籍或者文章。
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位的组合情况,如表2-1所示。
表2-1 对象头信息
另外在源代码中我们还看到一个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),如果是的话继续,即使误判之后也没关系,这相当于是浮动垃圾,在下一次回收的时候仍然可能被回收。
- 点赞
- 收藏
- 关注作者
评论(0)