对象头

class oopDesc {
 ...
private:
  volatile markOop _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  ...
}

在hotspot中对象指针称为oop(ordinary object pointer),而oopDesc则是对象头的结构.。除了Klass(之所以叫klass是因为class是C++关键字)指针外,,还由一个_mark字段,,是因为除了对象的class信息以外,还有一些对象信息需要保留, 比如GC年龄, 锁状态等。

markOop用于存储运行时自身的数据,包括哈希码、GC分代年龄、偏向锁标记、线程持有的锁、偏向线程ID、偏向时间戳等。从markOop.hpp中的注释中,我们可以详细的了解到Markword各个bit位所代表的内容和含义。以32位的系统为例,能总结出如下表:

Hotspot对象的内存布局-LMLPHP

Hotspot对象的内存布局-LMLPHP

其中轻量级锁和偏向锁是Java6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示  

Hotspot对象的内存布局-LMLPHP

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析)。

_klass是存在于union类型的_metadata中的,union类型的分配是按成员最大的那个进行分配的, 然后对这块内存的解释取决于代码中使用的是哪个字段。

typedef juint  narrowKlass; // typedef uint32_t juint;

typedef class markOopDesc* markOop;

对象头默认情况占16字节,在开启压缩对象指针时(通过-XX:+UseCompressedClassPointers),占12字节,默认状态是开启的. 

对象成员

  • long / double - 8 bytes
  • int / float - 4 bytes
  • short / char - 2 bytes
  • byte/boolean - 1 bytes
  • reference type - 4 or 8 bytes

heap小于32G时, 指针压缩默认开启.。JVM相应的控制参数为: -XX:+/-UseCompressedOops。

对象局部

与对象内存布局相关的命令如下:

(1)-XX:+UseCompressedClassPointers。

(2)-XX:+/-UseCompressedOops。

(3)-XX:PrintFieldLayout可查看对象的内存布局。

(4)-XX:FieldsAllocationStyle=mode, 默认mode是1。

(5)-XX:+/-CompactFields 由于填充会形成gap空洞, 比如使用压缩kclass指针时, 头占12字节, 后面如果是long的话, long的对齐要求是8字节, 中间会有4个字节的空洞, 为了高效利用, 可以把int/short/byte等比较小的对象塞进去, 与此同时JVM提供了开关控制该特性-XX:+/-CompactFields, 默认开启。

参考:

(1)https://blog.csdn.net/lqp276/article/details/52190503

(2)https://www.cnblogs.com/duanxz/tag/JVM%E8%99%9A%E6%8B%9F%E6%9C%BA/

(3)JVM之压缩指针(CompressedOops)https://juejin.im/post/5c4c8ad9f265da6179752b03

05-11 22:12
查看更多