Java 对象、对象头mark word、锁升级、对象占内存大小

2,402

java对象的组成

一个Java对象是在堆内存中,由对象头(Header),实例数据(Instance Data)和对齐填充(Padding)三部分组成,

  • 对象头由标记字(mark word)、类指针(klass word)和 数组长度组成
    mark word 下面重点介绍。
    klass word 是指向该对象类元数据(方法区)的指针,JVM通过这个指针确定对象是哪个类的实例。
    数组长度: 如果对象是一个数组,那么对象头还需要有额外的空间来存储数组的长度。

  • 实例数据主要包括对象的各种成员变量,包括基本类型和引用类型。基本类型直接存储内容,引用类型则是存储的指针,static类型的变量会放到类中,而不是放到实例数据里。

  • 对齐填充主要作用是提高CPU内存访问速度,可以参考理一理字节对齐的那些事

总结如下图:

再盗张图,开启压缩,使用选项+UseCompressedOops,klass word和数组长度都会变成4个字节

对象头 mark word

mark word主要用来表示对象的线程锁状态,另外还可以用来配合GC、以及存放该对象的hashCode 以64系统为例,mark word是64bit来表示:

image.png

先看锁标志位和偏向锁标记位:
最低2位,锁标志位(lock)是表示对象的线程锁状态,其中,正常和偏向锁时都是01,轻量级锁用00表示,重量级锁用10表示,标记了GC的用11表示。由于正常和偏向锁时都是01,因此低3位 偏向锁标记位(biased_lock) 用0或1表示是否时偏向的。

接着我们横着看,

  • 在正常不加锁时,mark word 由lock、biased_lock、age、identity_hashcode组成,age是GC的年龄,最大15(4位),每从Survivor区复制一次,年龄增加1。identity_hashcode就是对象的哈希码,当对象处于加锁状态时,这个哈希码会移到monitor,(synchronized会在代码块前后插入monitor)。

  • 在偏向锁时,mark word 由lock、biased_lock、age、epoch、thread组成。
    epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
    thread:持有偏向锁的线程ID,如果该线程再次访问这个锁的代码块,可以直接访问。

  • 在轻量级锁时,mark word 由lock、ptr_to_lock_record组成。
    ptr_to_lock_record:指向栈中锁记录的指针。

  • 在重量级锁时,mark word 由lock、ptr_to_heavyweight_monitor组成。
    ptr_to_heavyweight_monitor:指向对象监视器Monitor的指针。

synchronized 锁升级

锁升级就是lock状态从正常无锁->偏向锁->轻量级锁->重量级锁的过程

  1. 初期锁对象刚创建时,还没有任何线程来竞争,锁状态01,偏向锁标识位是0(无线程竞争它)。

  2. 当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。

  3. 当有两个线程开始竞争这个锁对象,情况发生变化了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。轻量级锁在加锁过程中,用到了自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。

  4. 如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。Monitor依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

具体可参考synchronized锁升级原理分析

锁的优缺点对比

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步代码块仅存在纳秒级差距如果线程间存在锁竞争,会带来额外的锁撤销消耗适用于只有一个线程访问同步代码块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁,使用自旋会消耗CPU追求响应时间;同步代码块执行时间非常短
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量;同步代码块执行时间较长

对象占内存大小

利用JOL包查看对象的布局和对象占内存多少
具体参考下面两篇文章:
Only HotSpot/OpenJDK VMs are supported

JOL:查看Java 对象布局、大小工具
jol使用

另外可使用Instrumentation进行计算,可参考另外可使用Instrumentation进行计算