Java对象头Object Header、偏向锁、轻量锁、重量锁研究

1 JAVA对象头的组成

1.1 理论研究

我们都知道,Java对象存储在堆(Heap)内存。那么一个Java对象到底包含什么呢?概括起来分为对象头、对象体和对齐字节。如下图所示:

img

对象的几个部分的作用:

1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;

2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

5.对齐字节,是为了因为对象的大小必须是Word字长的倍数(64bit的JVM其word长度为64bit既8字节)所以需要一定的字节进行拼凑。

从官方文档:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
Java对象头Object Header、偏向锁、轻量锁、重量锁研究_第1张图片

里面有关于Object header的表述。翻译大概如下:

object header: 每个被gc管理的堆对象在内存中都具有一个开始的公共结构,既对象头。每个oop(指向对象的指针)会指向一个对象头实现对object的引用。对象头包括关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。由两个Word组成。若对象为数组,紧随其后的是数字长度字段。注意,Java对象和vm内部对象都有通用的对象头格式。

上面提到ojbect header的包括以下内容:

  • mark word是对象的第一个word(长度64bit),是一些bit域来存放对象的sync同步锁状态、hashcode,已经同步锁相关的新,GC信息状态等。
  • klass pointer是对象的第二个word(长度也是64bit),存储指针指向对象原型定义(Java类的内部表示,在Metaspace元数据区)。 虽然通常是64bit,但如果JVM启用了JVM指针压缩机制,则实际可能被压缩为32bit。
  • 若对象为数组,紧随其后的是数字长度字段。

注意:关于word字长。这里的word是一个抽象的概念,不同的地方有不同的含义,不同的长度。

比如cpu字长是指cpu同时参与运算的二进制位数,现在主流的pc的机器字长都是64bit的。机器字长直接决定着机器可寻址的虚拟空间地址大小。

对于jvm的字长,jvm specification e7并没有找到jvm定义字长,但是在《深入Java虚拟机》这本老经典里头,看到作者写到

The basic unit of size for data values in the Java virtual machine is the word--a fixed size chosen by the designer of each Java virtual machine implementation(字长选择取决于JVM的实现者). The word size must be large enough to hold a value of type byte, short, int, char, float, returnAddress, or reference. Two words must be large enough to hold a value of type long or double. An implementation designer must therefore choose a word size that is at least 32 bits(字长至少为32bit), but otherwise can pick whatever word size will yield the most efficient implementation. The word size is often chosen to be the size of a native pointer on the host platform.  
  
The specification of many of the Java virtual machine's runtime data areas are based upon this abstract concept of a word. For example, two sections of a Java stack frame--the local variables and operand stack-- are defined in terms of words. These areas can contain values of any of the virtual machine's data types. When placed into the local variables or operand stack, a value occupies either one or two words.  
  
As they run, Java programs cannot determine the word size of their host virtual machine implementation. The word size does not affect the behavior of a program. It is only an internal attribute of a virtual machine implementation.  

一般字长选择都根据底层主机平台的指针长度来选择,而指针长度是由cpu运行模式的寻址位数决定的,所以64位的物理机器上,运行64位OS,安装64位JVM的话,其Word的大小通常也是64bit的(当然这根实际使用内存大小和jvm参数设置相关)。 这里可以记住一个经验值:对于64bit的JVM,每个Word大大小通常就是64bit。

1.2 对象头结构

通过源代码src/share/vm/oops/markOop.hpp文件分析(http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/markOop.hpp),按照big endian大端顺序的object header如下:

object header Mark word(64bit) klass pointer(64bit)
normal object 普通对象,无锁。 unused:25|identity_hashcode:31|unused:1|age:4|biased_lock:1|lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩
biased object 带有偏向锁的对象 thread:54 |epoch:2 |unused:1|age:4|biased_lock:1|lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩
带有轻量锁的对象 prt_to_lock_record:62 |lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩
带有重量锁的对象 prt_to_heavyweigth_monitor:62 |lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩
GC |lock:2 oop to metadata object 指向对象的元定义,可能会被指针指针压缩

java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态,无锁状态、加锁状态(又细分为偏向锁、轻量锁、重量锁)、gc标记状态。

1.2.1 markword结构

概括起来markword中各个的含义为:

  • unused:表示未使用。

  • identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算后,才会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。

  • 4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

  • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。

  • lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,锁状态含义不同情况表示的含义如下表

    img

  • thread:持有偏向锁的线程ID。

  • epoch:偏向锁的时间戳。

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

1.2.2 Klass point(类指针)

​ 这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  • 每个Class的属性指针(即静态变量)
  • 每个对象的属性指针(即对象变量)
  • 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向Metaspace的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

1.2.3 数组长度

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

1.3 锁类型

我们通常说的通过synchronized实现的同步锁,真实名称叫做重量锁(在Java 1.6之前)。重量锁会直接使用操作系统的底层的锁,会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。为了提高效率,从Java 1.6开始,JVM进行了优化,synchronized不一定直接使用重量锁,引入了偏向锁。具体为:

​ 建议先看一文《图解 偏向锁,轻量锁,重量锁》:https://xvshu.blog.csdn.net/article/details/88039489

1.3.1 偏向锁(Biased Locking)

偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。

也就是说:

在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:

  1. 判断一下当前线程id是否与对象的Markword当中的线程id是否一致.
  2. 如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.
  3. 如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。
  4. 如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。
  5. 如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。

可以看出,偏向锁是针对于单个线程而言的,线程获得锁之后就不会再有解锁等操作了,这样可以省略很多开销。假如有两个线程来竞争该锁话,那么偏向锁就失效了,进而升级成轻量级锁了。

为什么要这样做呢?因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的。这也是为什么会有偏向锁出现的原因。

JVM启动时,偏向锁的开关是默认开启的,可以使用-XX:+UseBiasedLocking 开启和关闭。

JVM启动时,偏向锁立即使用而且启动几秒后延迟才能使用。可以使用-XX:BiasedLockingStartupDelay设置秒数,默认为4秒。

一旦对象计算了hashcode,就不能使用偏向锁。因为markword的数据被占用了。

1.3.2 轻量锁

偏向锁假设只有单线程访问,如果真的只有单线程下则避免了资源同步,提高了性能。 但是万一不止一个线程范围,还有其他线程访问,那么偏向锁无法解决,则需要升级为轻量锁。

​ 顾名思义,轻量是相对于重量锁,使用轻量锁时,不需要申请操作系统的互斥量(mutex)。而是将mark word中的信息复制到当前线程的栈中,然后通过cas尝试修改mark word并替换成轻量锁,如果替换成功则执行同步代码。如果此时有线程2来竞争,并且他也尝试cas修改mark word但是失败了,那么线程2会进入自旋状态(既自旋锁)。

  • 自旋锁

    所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。 经验表明,大部分同步代码块执行的时间都是很短很短的,也正是基于这个原因,才有了轻量级锁这么个东西。

    自旋锁的一些问题
    1. 如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在原地等待空消耗cpu,这会让人很难受。
    2. 本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。

    基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁

    自旋锁是在JDK1.4.2的时候引入的

    默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改。

    自适应自旋锁

    所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。

    其大概原理是这样的:

    假如一个线程A刚刚成功获得一个锁,当它把锁释放了之后,线程B获得该锁,并且线程B在运行的过程中,此时线程A又想来获得该锁了,但线程B还没有释放该锁,所以线程A只能自旋等待,但是虚拟机认为,由于线程A刚刚获得过该锁,那么虚拟机觉得线程A这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程A自旋的次数

    另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。

    轻量级锁也被称为非阻塞同步乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待,串行执行。

1.3.3 重量锁

轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。。

为什么说重量级锁开销大呢

主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。这就是说为什么重量级线程开销很大的。

互斥锁(重量级锁)也称为阻塞同步悲观锁

1.3.4 锁类型总结

**为什么JVM要优化synchronized,是因为发现在实际情况情况资源争用,不一定都要使用重量锁才能解决,所有才在Java1.4引入了轻量锁,1.6引入了偏向锁。**所以,才有了锁逐步升级,JVM的想法是先尽量尝试性能高效的,如果不行再逐步升级。也是从偏向锁,轻量级锁,再到重量级锁的过程。

  1. 先假设只有单线程访问(偏向锁),这样连同步都省略了。
  2. 万一不是单线程访问,则升级为轻量锁。通过自旋反复尝试(自旋是非阻塞的,需要消耗CPU),这时再JVM内部,无需借助操作系统的MutexLock。
  3. 如果多次自旋还还不行,就升级为重量锁。借用操作系统的MutexLock,避免消耗CPU,使用MutexLock的阻塞或者唤醒机制。

这个过程也告诉我们,假如我们一开始就知道某个同步代码块的竞争很激烈、很慢的话,那么我们一开始就应该使用重量级锁了,从而省掉一些锁转换的开销。

1.4 锁升级

JVM的想法是先尽量尝试性能高效的,如果不行再逐步升级。也是从偏向锁,轻量级锁,再到重量级锁的过程,如下步骤:

  1. 初期对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。 其中hashCode需要调用hashCode()方法后才会赋值。

  2. 当有一个线程来拿本对象的锁,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图第二种情形。

  3. 当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。如下图第三种情形。

  4. 如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。如下图第四种情形。

    img

2 JOL分析工具和环境

​ JOL全称为Java Object Layout,是分析JVM中对象布局的工具,该工具大量使用了Unsafe、JVMTI来解码布局情况,所以分析结果是比较精准的。使用该工具,只需要将jol的jar报文引入工程即可。具体如下:

已经安装JDK 64bit后,在eclipse新建一个Maven工程。然后在pom.xml加入以下引用,以使用JOL工具。

		
		<dependency>
		    <groupId>org.openjdk.jolgroupId>
		    <artifactId>jol-coreartifactId>
		    <version>0.10version>
		dependency>

新建一个被分析的类CA.java,只定义简单的两个属性, 代码如下:

package com.zyp.StudyObjHeader;

public class CA {
    private boolean flag = false;
    private int number = 16;
}

再新建一个测试类JOLExample1.java,代码如下:

package com.zyp.StudyObjHeader;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

public class JOLExample1 {

    public static void main(String[] args) {
        CA  obj1 = new CA(); // 先创建一个对象
        System.out.println("----------- JVM details ------------");
        System.out.println(VM.current().details());
        System.out.println("----------- obj1 details ------------");
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
    }
}

工程结构为:
Java对象头Object Header、偏向锁、轻量锁、重量锁研究_第2张图片

3 分析

3.1 运行结果

运行测试类JOLExample1,执行结果如下

----------- JVM details ------------
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

----------- obj1 details ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

3.2 分析结果

JVM虚拟机信息

  • 可以看到使用了压缩指针。 (所有后面的klass point不是64bit,被压缩为32bit)
  • Objects are 8 bytes aligned,这意味着所有的对象分配的字节都是8的整数倍,既上文的Word字长为8字节(64bit)。
  • Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 对应:[Oop(Ordinary Object Pointer), boolean, byte, char, short, int, float, long, double]大小。

Object 信息

整个obj1对象一共24个Byte,其中:

  • ojbect header占用12个Byte。 (8个Byte的markword,和被压缩后的klasss point为4个Byte)

  • 对象实例数据占用5个Byte。

  • 对齐占用了7个字节(因为在64位虚拟机上对象的大小必须是Word字长的倍数,既8字节的倍数)

3.3 分析Object header

3.3.1 观察hashcode的变化

在无锁的情况下,object header中的前64bit是markword。 markword的前56bit存的是对象的hashcode,那么来验证一下 。

再新建一个测试类JOLExample2.java,代码如下:

package com.zyp.StudyObjHeader;

import java.nio.ByteOrder;

import org.openjdk.jol.info.ClassLayout;

public class JOLExample2 {

    public static void main(String[] args) {
        System.out.println("nativeOrder:" + ByteOrder.nativeOrder()); //查看当前JVM使用字节序是大端、小端。
        CA  obj1 = new CA(); // 先创建一个对象
        System.out.println("----------- before hash ------------");
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
        
        System.out.println("obj1 hashcode is: " + Integer.toBinaryString(obj1.hashCode()));
        
        System.out.println("----------- after hash ------------");
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
    }
}

运行结果如下:

nativeOrder:LITTLE_ENDIAN    (注意:当前JVM是小端模式)
----------- before hash ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

obj1 hashcode is: 1100001 11100100 01110000 01011011   (为了便于看,人为加了分隔符)
----------- after hash ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 5b 70 e4 (00000001 01011011 01110000 11100100) (-462398719)
      4     4           (object header)                           61 00 00 00 (01100001 00000000 00000000 00000000) (97)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total


既结果为:

对象Markword(小端)
计算hashcode前 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
计算hashcode后 00000001 01011011 01110000 11100100 01100001 00000000 00000000 00000000

由于当前JVM是小端模式,所以64bit(8个字节)的markword的高低顺序,以Byte为单位是反序的。 对称为了便于分析,我们转为大端,结果如下表:

对象Markword(转为大端后)
计算hashcode前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
计算hashcode后 00000000 00000000 00000000 01100001 11100100 01110000 01011011 00000001

转换为大端,可以发现:

  • 在对象未调用hashcode()方法前,markword中的值全是零(无值),调用hashcode()方法计算后,才将值保存到markword中。
  • 前25个bit为:unused, 所以全0
  • 然后为31个bit的:identity_hashcode (图中加粗的字体),值为1100001 11100100 01110000 01011011,等于调用hashcode()方法得到的值
  • 后面1个bit为:unused, 值为0
  • 再4个bit为:age, GC中分代年龄。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因
  • 再1个bit为:biased_lock,值为0,表示无偏向锁。
  • 在2个bit为:lock状态,值为01,表示未锁定。

特别注意:一旦对象计算了hashcode,就不能使用偏向锁。因为markword的数据被占用了。

3.3.2 观察age的变化

再新建一个测试类JOLExample3.java,代码如下:

package com.zyp.StudyObjHeader;

import java.nio.ByteOrder;

import org.openjdk.jol.info.ClassLayout;

public class JOLExample3 {

    public static void main(String[] args) {
        System.out.println("nativeOrder:" + ByteOrder.nativeOrder()); //查看当前JVM使用字节序是大端、小端。
        CA  obj1 = new CA(); // 先创建一个对象
        System.out.println("-----------  before GC ------------");
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
        
        System.gc(); // 人工发起GC
        System.out.println("----------- after GC ------------");
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
    }
}

运行结果如下:

nativeOrder:LITTLE_ENDIAN
-----------  before GC ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

----------- after GC ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

既结果为:

对象Markword(小端)
GC前 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
GC后 00001001 00000000 00000000 00000000 00000000 00000000 00000000 00000000

由于当前JVM是小端模式,所以64bit(8个字节)的markword的高低顺序,以Byte为单位是反序的。 对称为了便于分析,我们转为大端,结果如下表:

对象Markword(转为大端后)
GC前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
GC后 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001001

可以看到大端的最后一个字节数据:从00000001变成了00001001,既age增加了1。

3.3.3 观察有锁的情况

3.3.3.1 偏向锁

再新建一个测试类JOLExample4.java,代码如下:

package com.zyp.StudyObjHeader;

import java.nio.ByteOrder;

import org.openjdk.jol.info.ClassLayout;

public class JOLExample4 {

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(4500);  // 先睡眠大于4秒, 因为-XX:BiasedLockingStartupDelay设置秒数默认为4秒,JVM启动4秒后才允许有偏向锁
        System.out.println("nativeOrder:" + ByteOrder.nativeOrder()); //查看当前JVM使用字节序是大端、小端。
        CA  obj1 = new CA(); // 先创建一个对象
        System.out.println("----------- before lock ------------");
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
        
        synchronized (obj1) {
            System.out.println("----------- locking ------------");
            System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
        }
        
        System.out.println("----------- after lock(Biased Locking) ------------");
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
    }
}

运行结果如下:

nativeOrder:LITTLE_ENDIAN
----------- before lock ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

----------- locking ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 e0 95 02 (00000101 11100000 10010101 00000010) (43376645)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

----------- after lock(Biased Locking) ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 e0 95 02 (00000101 11100000 10010101 00000010) (43376645)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

此时使用的偏向锁,既结果为:

对象Markword(小端)
lock前 00000101 00000000 00000000 00000000 00000000 00000000 00000000 00000000
lock中 00000101 11100000 10010101 00000010 00000000 00000000 00000000 00000000
lock后 00000101 11100000 10010101 00000010 00000000 00000000 00000000 00000000

由于当前JVM是小端模式,所以64bit(8个字节)的markword的高低顺序,以Byte为单位是反序的。 对称为了便于分析,我们转为大端,结果如下表:

对象Markword(转为大端后)
lock前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
lock中 00000000 00000000 00000000 00000000 00000010 10010101 11100000 00000101
lock后 00000000 00000000 00000000 00000000 00000010 10010101 11100000 00000101

可以看到:先默认加上了偏向锁,lock中还是偏向锁,且使用后偏向锁状态仍然保持。

偏向锁,也可以理解为“偏心”,偏心于某个线程:

  • lock前默认加上了偏向锁(末尾为101),但这是没有任何线程使用,则前54个bit初始赋值为0, 表示“可偏向的”。

  • lock后尽管已经退出了资源同步块,既该线程不使用锁了。 但由于是偏向锁,它还是偏向刚才使用的线程,以便该线程下一次再次使用。这时,保持偏向锁状态(末尾为101),同时前54个bit值仍继续保持刚才的线程ID。 从markword角度,也充分体现了偏向锁的特征,体现了JVM优先使用偏向锁的假设:假设只有一个线程访问资源同步块。

特别注意:一旦对象计算了hashcode,就不能使用偏向锁。因为markword的数据被占用了。

3.3.3.2 轻量锁

将上面的Java代码中Thread.sleep的加上注释,不睡眠,直接运行。

运行结果如下:

nativeOrder:LITTLE_ENDIAN
----------- before lock ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

----------- locking ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           60 f1 0e 03 (01100000 11110001 00001110 00000011) (51310944)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

----------- after lock(Biased Locking) ------------
com.zyp.StudyObjHeader.CA object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int CA.number                                 16
     16     1   boolean CA.flag                                   false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

可以看到after lock后,并markword上没有加锁信息。 而在locking中,使用了轻量锁(第一个字节末尾为00),为什么?

因为JVM参数-XX:BiasedLockingStartupDelay设置秒数默认为4秒,既JVM启动4秒后才允许有偏向锁。 没有sleep睡眠,无法使用偏向锁,所以此时使用的是轻量锁,既结果为:

对象Markword(小端)
lock前 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
lock中 01100000 11110001 00001110 00000011 00000000 00000000 00000000 00000000
lock后 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000

由于当前JVM是小端模式,所以64bit(8个字节)的markword的高低顺序,以Byte为单位是反序的。 对称为了便于分析,我们转为大端,结果如下表:

对象Markword(转为大端后)
lock前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
lock中 00000000 00000000 00000000 00000000 00000011 00001110 11110001 01100000
lock后 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

可以看到:先无锁,然后使用轻量锁,使用后轻量锁会释放。

3.3.3.3 重量锁

再新建一个测试类JOLExample5.java,代码如下:

package com.zyp.StudyObjHeader;

import org.openjdk.jol.info.ClassLayout;

class OBJ { 
    boolean flag = false;
}

public class JOLExample5 {
    static OBJ obj_c;
  
    public static void main(String[] args) throws InterruptedException {
        obj_c = new OBJ();
        System.out.println("********** before lock **********");
        System.out.println(ClassLayout.parseInstance(obj_c).toPrintable());
 
 
        Thread t1 = new Thread() {  // 定义一个子线程t1
            @Override
            public void run() {
                synchronized (obj_c) {
                    try {
                        System.out.println(" ********** t1 thread locking... ********** ");
                        System.out.println(ClassLayout.parseInstance(obj_c).toPrintable());
                        Thread.sleep(8000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
 
        t1.start();
        Thread.sleep(500); // 主线程先睡一会,让子线程先执行起来。

        System.out.println(" ********************  ");
        main_sync();
 
        System.out.println(" ********** after lock 1 (should be unlock, but ...) **********");
        System.out.println(ClassLayout.parseInstance(obj_c).toPrintable());
        Thread.sleep(2000); // 主线程先睡一会,让重量锁充分释放(几百毫秒有时也可以,但不一定)。
        System.out.println(" ********** after lock 2 (should be unlock) **********");
        System.out.println(ClassLayout.parseInstance(obj_c).toPrintable());
 
        System.gc();
        System.out.println(" ********** after gc **********");
        System.out.println(ClassLayout.parseInstance(obj_c).toPrintable());
 
    }
 
 
    public static void main_sync() {
        synchronized (obj_c) {
            System.out.println(" ********** main thread locking **********");
            System.out.println(ClassLayout.parseInstance(obj_c).toPrintable());
        }
    }
}

运行结果如下:

********** before lock **********
com.zyp.StudyObjHeader.OBJ object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           9f c0 00 f8 (10011111 11000000 00000000 11111000) (-134168417)
     12     1   boolean OBJ.flag                                  false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

 ********** t1 thread locking... **********
com.zyp.StudyObjHeader.OBJ object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           b0 f1 12 25 (10110000 11110001 00010010 00100101) (621998512)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           9f c0 00 f8 (10011111 11000000 00000000 11111000) (-134168417)
     12     1   boolean OBJ.flag                                  false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

 ********************  
 ********** main thread locking **********
com.zyp.StudyObjHeader.OBJ object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           7a 1d a7 21 (01111010 00011101 10100111 00100001) (564600186)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           9f c0 00 f8 (10011111 11000000 00000000 11111000) (-134168417)
     12     1   boolean OBJ.flag                                  false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

 ********** after lock 1 (should be unlock, but ...) **********
com.zyp.StudyObjHeader.OBJ object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           7a 1d a7 21 (01111010 00011101 10100111 00100001) (564600186)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           9f c0 00 f8 (10011111 11000000 00000000 11111000) (-134168417)
     12     1   boolean OBJ.flag                                  false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

 ********** after lock 2 (should be unlock) **********
com.zyp.StudyObjHeader.OBJ object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           9f c0 00 f8 (10011111 11000000 00000000 11111000) (-134168417)
     12     1   boolean OBJ.flag                                  false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

 ********** after gc **********
com.zyp.StudyObjHeader.OBJ object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           9f c0 00 f8 (10011111 11000000 00000000 11111000) (-134168417)
     12     1   boolean OBJ.flag                                  false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

由于当前JVM是小端模式,所以64bit(8个字节)的markword的高低顺序,以Byte为单位是反序的。 对称为了便于分析,我们转为大端,结果如下表:

对象Markword(转为大端后)
befor lock 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (无锁)
t1 thread locking 00000000 00000000 00000000 00000000 00100101 00010010 11110001 10110000 (轻量锁)
main thread locking 00000000 00000000 00000000 00000000 00100001 10100111 00011101 01111010 (重量锁)
after lock 1 00000000 00000000 00000000 00000000 00100001 10100111 00011101 01111010 (重量锁还未释放)
after lock 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (无锁,重量锁已释放)
after gc 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001001 (无锁。age=1)

可以看到从从轻量锁到重量锁的升级过程,及释放过程

另外:如果在Java代码中调用了wait()方法进入阻塞,因为阻塞需要调用操作系统的函数,所以调用wait()后会升级为重量锁。

3.3.3 偏向锁和轻量锁的性能对比

新建一个被分析的类CB.java,只定义简单的两个属性, 代码如下:

package com.zyp.StudyObjHeader;

public class CB {
    private int counter = 0;

    synchronized public void dosomething() {    
        counter ++;  // 随便做点事情,这是在同步块内
    }
}

再新建一个测试类JOLExample6.java,代码如下:

package com.zyp.StudyObjHeader;

import java.nio.ByteOrder;

import org.openjdk.jol.info.ClassLayout;

public class JOLExample6 {

    public static void main(String[] args) throws InterruptedException {
        //Thread.sleep(4500);  // 先睡眠大于4秒, 因为-XX:BiasedLockingStartupDelay设置秒数默认为4秒,JVM启动4秒后才允许有偏向锁
        System.out.println("nativeOrder:" + ByteOrder.nativeOrder()); //查看当前JVM使用字节序是大端、小端。
        CB  obj_b = new CB(); // 先创建一个对象
        
        System.out.println("----------- before lock ------------");
        System.out.println(ClassLayout.parseInstance(obj_b).toPrintable());
        
        long start_time = System.currentTimeMillis();
        for(int i = 0; i < 1000000000L; i++) {
            obj_b.dosomething();
        }
        long end_time = System.currentTimeMillis();
        System.out.println(" totoal elapsed time: [" + (end_time-start_time) + "]ms");
        System.out.println("----------- after lock ------------");
        System.out.println(ClassLayout.parseInstance(obj_b).toPrintable());
    }
}

执行结果如下。由于启动马上执行,所以使用的是轻量锁,使用轻量锁的耗时为18.844秒`。 轻量锁会释放,因此before和after都显示无锁。

nativeOrder:LITTLE_ENDIAN
----------- before lock ------------
com.zyp.StudyObjHeader.CB object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4    int CB.counter                                0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

 totoal elapsed time: [18844]ms
----------- after lock ------------
com.zyp.StudyObjHeader.CB object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4    int CB.counter                                1000000000
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

如果将上面的Java代码中,将Thread.sleep的注释去掉,先睡眠一会。

再次运行结果如下。 这是则使用了偏向锁,,使用偏向锁的耗时为2.073秒。 偏向锁会会默认加上,且不会释放,因此before和after都显示偏向锁。

nativeOrder:LITTLE_ENDIAN
----------- before lock ------------
com.zyp.StudyObjHeader.CB object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4    int CB.counter                                0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

 totoal elapsed time: [2073]ms
----------- after lock ------------
com.zyp.StudyObjHeader.CB object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 e0 b7 02 (00000101 11100000 10110111 00000010) (45604869)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4    int CB.counter                                1000000000
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以发现偏向锁性能明细高于轻量锁(接近10倍),这也是为什么JVM要优化的原因。

3.3.4 批量重定向

批量重偏向(bulk rebias)机制

​ 一种场景:一个A线程创建了大量对象objs(这些objs对象都属于同一个class)并执行了初始的同步操作(这些大量对象objs的锁状态为偏向锁,markword中的TheadID=线程A),后来另一个B线程也将这些对象objs作为锁对象进行操作,这时应进行锁升级,从偏向锁升级到轻量锁,既撤销偏向锁并升级为轻量锁。由于涉及大量objs对象锁升级,这样会导致大量的偏向锁撤销操作。

​ 对此,JVM进行优化,对于这种情况(bjs对象都属于同一个class)的大量锁升级,可以优化为不进行锁升级至轻量锁(也不进行偏向锁撤销),保持偏向锁,但直接把这些objs的对象头中markword中的TheadID从线程A改为B即可。既直接切换为有线程B持有的偏向锁。既重偏向

具体的实现过程

  1. 以class为单位,为每个class维护一个偏向锁撤销计数器。该class下的所有实例化对象,都共享这个全局计数器。
  2. 该class的实例化对象,每发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
  3. 当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

你可能感兴趣的:(JVM)