浅析Java对象头结构和synchronized关键字原理

Java对象的内存布局


在Hotspot虚拟机中,堆中的Java对象由以下三部分组成:

 1. 对象头
 2. 实例数据
 3. 对齐填充

事实上实例数据部分存储的就是我们在对象中定义的实例成员变量包括从父类中继承过来的(不包括类成员,它们在方法区中分配),实例成员变量包括基本数据类型和引用数据类型,基本数据类型在对象中存储的就是这个值,而引用数据类型我们存储的只是它的引用。这也就是为什么一个类的对象在类加载完成后我们就知道了它的大小了,第一次在周志华老师的《深入理解Java虚拟机》一书中读到这句话我很费解,一个Collection容器对象我们如何做到一加载就知道它的大小呢,随着容器对象存储的数据越来越多它不是应该在不断的膨胀变大吗? 事实上不是这样的,以Arraylist对象为例,它的底层是用数组实现的,在ArrayList对象中存储的仅仅只是一个指向数组的引用,不管数组多大ArrayList对象本体大小都是从一开始就确定的。

对齐填充没有什么特殊的含义,仅仅起到占位符的作用。因为HotSpot虚拟机的自动内存管理系统要求对象的大小必须是8字节的整数倍。后面我们会提到,对象头的数据一定是8的整数倍,所以当对象的实例数据部分没有对齐时需要通过对其填充来补全。

本文第一部分我们重点讨论对象头的结构,Java对象头有两种类型:

  1. 普通对象(非数组类型):Mark Word、Klass Pointer;
  2. 数组对象:Mark Word、Klass Pointer、Array Length;
HotSpot虚拟机头像头占用内存大小
数据类型32位JVM64位JVM开启指针压缩的64位JVM存储内容
Mark Word326464 对象的运行时数据:如HashCode, GC分代年龄,锁状态标记等
Klass Pointer326432对象指向它的类元数据的指针,通过这个指针确定这个对象是哪个类的实例
Array Length323232如果是数组对象,记录数组的长度

可见在64位JVM中开启指针压缩(-XX:UseCompressedOops)后, JVM只是针对类型指针(Klass Pointer)进行压缩。而数组长度不管在什么类型的JVM里都是32bit,Mark Word 和虚拟机位数有关,占用一个Word的大小。因为对象头中定义的信息大多是与对象本身定义的数据无关的额外信息,对于虚拟机来说这一部分额外开销还是比较占空间的。考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,它会根据对象的状态复用自己的空间。对象的状态如何确定?由Mark Word最低的两个bits确定,如下:

对象状态
标志位状态
01未锁定
00轻量级锁定
10重量级锁定
11GC标记
01可偏向

 

32位JVM的Mark Word布局如下:

32位JVM Mark Word
data(30 bits)state(2 bits)

identify_hashcode(25)    |    age(4)   |     biased_lock(1)

(01)无锁
thread(23) |    epoch(2)     | age(4) | biased_lock(1)(01)偏向锁
ptr_to_lock_record(30)(00)轻量级锁
ptr_to_heavyweight_monitor(30)(10)重量级锁
unUsed(11)GC标记
  • 名词解释:

    • age: GC分代年龄
    • identify_hashcode: 对象的hashcode值
    • threadId: 偏向线程的Id
    • biased_lock: 是否是偏向锁,因为只占一个bit,所以只有0和1
    • epoch: 偏向时间戳
    • ptr_to_lock_record: 指向栈中轻量级锁记录的指针
    • ptr_to_heavyweight_monitor:指向栈中重量级锁的指针
    • GC标记: 用于GC算法对对象的标记
    • gc_info: GC算法给不同状态的标记信息
  • 为什么要这么实现?

    1. 因为对象头信息是跟对象自身定义的数据结构无关的。这些信息所记录的状态是用于JVM对对象的管理的。更重要的是,不同状态的存储内容基本上是互斥的。所以基于节省空间的角度考虑,Mark Word 被设计成动态的。
  • identify_hashcode 既然有方法可以生成为什么要放在对象头里?

    1. 当一个对象的hashCode()未被重写时,调用这个方法会返回一个由随机数算法生成的值。因为一个对象的hashCode不可变,所以需要存到对象头中。当再次调用该方法时,会直接返回对象头中的hashcode。
    2. identify_hashcode 采用延迟加载的方式生成。只有调用hashcode()时,才会写入对象头。若一个类的hashCode()方法被重写,对象头中将不存储hashcode信息,因为一般我们自己实现的hashcode()并未将生成的值写入对象头。
  • 当对象的状态不是默认状态时,对象的hashcode去哪儿了?

    1. 当是轻量级锁/重量级锁时,jvm会将对象的 mark word 复制一份到栈帧的Lock Record中。 等线程释放该对象时,再重新复制给对象。
    2. 如果一个对象头中存在hashcode,则无法使用偏向锁。


64位JVM的Mark Word的情况类似,不再赘述:

64位JVM
data(30 bits)state(2 bits)
unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1(01)无锁
    thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1(01)偏向锁
ptr_to_lock_record:62(00)轻量级锁
ptr_to_heavyweight_monitor:62 (10)重量级锁
unUsed(11)GC标记

Java对象头的内存布局介绍完以后,我们现在主要来看看synchronized重量级锁的情况。即Mark Word标记位10时。

Synchronized原理解析:

我们可以看到,当锁状态为10的时候Mark Word 存储的内容为ptr_to_heavyweight_monitor。翻译过来就是指向heavyweight Monitor对象的指针,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,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。上述分析是synchronized在JVM层面的原理,下面我们将进一步分析synchronized在Java层面的具体语义实现:

Snychronized代码块:

public class SyncronizedTest {

   public int i;

   public void syncBlockMethod(){
       synchronized (this){
           i++;
       }
   }
}

使用javap反编译工具得到字节码指令如下:

Compiled from "SyncronizedTest.java"
public class SyncronizedTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // SyncronizedTest.i:I
   #3 = Class              #20            // SyncronizedTest
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               i
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               syncBlockMethod
  #12 = Utf8               StackMapTable
  #13 = Class              #20            // SyncronizedTest
  #14 = Class              #21            // java/lang/Object
  #15 = Class              #22            // java/lang/Throwable
  #16 = Utf8               SourceFile
  #17 = Utf8               SyncronizedTest.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // i:I
  #20 = Utf8               SyncronizedTest
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/Throwable
{
  public int i;
    descriptor: I
    flags: ACC_PUBLIC

  public SyncronizedTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public void syncBlockMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field i:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 8: 14
        line 9: 24
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class SyncronizedTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}

我们主要关注字节码中的如下代码:

monitorenter
..........
monitorexit
..........
monitorexit

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

Synchronized同步方法:

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:

我们先定义一个同步方法:

public class SyncronizedTest {

   public int i;

   public synchronized void syncMethod(){
        i++;
   }
}

使用javap反编译工具得到字节码指令:

Compiled from "SyncronizedTest.java"
public class SyncronizedTest
{
  public int i;
    descriptor: I
    flags: ACC_PUBLIC

  public SyncronizedTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
}

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值