并发机制深入理解synchronized之(二)根据内存对象的布局了解锁机制

125 阅读13分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情

一、对象的内存布局

Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

image.png

image.png

1-1、对象头详解

HotSpot虚拟机的对象头包括:

  • Mark Word 

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。

  • Klass Pointer

对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。

  • 数组长度(只有数组对象有)

如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节

image.png

1-2、使用JOL工具查看内存布局

可以查看普通java对象的内部布局工具JOL(JAVA OBJECT LAYOUT),使用此工具可以查看new出来的一个java对象的内部布局,以及一个普通的java对象占用多少字节。

1-2-1、引入maven依赖

<!-- 查看Java 对象布局、大小工具 -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

1-2-2、测试使用(默认开启指针压缩)

1-2-2-1、查看空对象内存存储

  1. 利用jol查看64位系统java对象(空对象),默认开启指针压缩,总大小显示16字节,前12字节为对象头,因为对象存储占用字节需要是8的整数倍,目前只有12字节,因此需要有4个字节的填充位,这样总大小就是16字节 image.png

以上输出各列的含义如下:

  • OFFSET:偏移地址,单位字节;
  • SIZE:占用的内存大小,单位为字节;
  • TYPE DESCRIPTION:类型描述,其中object header为对象头;
  • VALUE:对应内存中当前存储的值,二进制32位;

1-2-2-2、查看非空内存对象存储

可以看到对象头前8个字节为mark word,第三个4字节为klass point,最后一8字节为Test对象中的为long类型的p属性,因为long占用8字节,这样加上对象头的12字节,就是20字节,因为需要为8的整数倍,就需要补充位为4字节。 image.png

1-2-2-3、查看非空内存对象存储(多属性)

可以看到除对象头的mark word和klass point之外的12字节,就是Test对象中的String类型b(String 占用4字节),和long类型的p(long 占用8字节),这样计算起来正好是24字节,填充位就没有了。 image.png

1-2-3、关闭指针压缩后

对象头为16字节:-XX:-UseCompressedOops

image.png

二、Mark Word是如何记录锁状态的

通过以上的了解,锁状态被记录在每个对象的对象头的Mark Word中 Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。

简单点理解就是:MarkWord 结构搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。

2-1、Mark Word的结构

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

。。。。。。
//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased
//
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
  • hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
  • age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
  • biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  • lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
  • JavaThread*: 保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
  • epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。

2-1-1、32位JVM下的对象结构描述

image.png

2-1-2、64位JVM下的对象结构描述

image.png

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针

2-2、Mark Word中锁标记枚举

enum {  locked_value             = 0,    //00 轻量级锁 
        unlocked_value           = 1,   //001 无锁
        monitor_value            = 2,   //10 监视器锁,也叫膨胀锁,也叫重量级锁
        marked_value             = 3,   //11 GC标记
        biased_lock_pattern      = 5    //101 偏向锁

更直观的理解方式:

image.png

32位和64位系统对象结构锁,都是以后两位来锁定对象锁的状态 image.png

2-3、测试:利用JOL工具跟踪锁标记变化

2-3-1、偏向锁

当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。

2-3-1-1、未添加锁状态查看数据对象头锁标记

再次看下刚刚代码运行的结果,锁状态标记为001即为无锁状态。 image.png

2-3-2-1、添加锁-查看对象头锁标记

分别输出obj在加锁前和加锁后的对象信息,首先给obj加锁 image.png

下图中对象信息,一开始是001->无锁状态;加锁之后是000->轻量级锁。此时obj本应该是偏向锁的,单为何输出的是轻量级锁呢?这就设计到了 偏向锁延迟偏向的机制 image.png

2-3-2-2、偏向锁延迟偏向

偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。

2-3-2-2-1、关闭延迟偏向锁
  • 方式一:设置JVM参数

关闭延迟偏向锁

//关闭延迟开启偏向锁 
-XX:BiasedLockingStartupDelay=0

启用/禁止偏向锁

//禁止偏向锁 
-XX:-UseBiasedLocking 
//启用偏向锁
-XX:+UseBiasedLocking

在Idea中设置JVM参数 image.png

启动测试,这样在未加锁的时候obj对象就启动了偏向锁(锁指向为空),添加线程之后(锁指向即为对应的ID)

image.png

  • 方式二:让线程等待5秒,让JVM先飞一会

因为Hotpoit虚拟机在启动后有个4秒延迟才会对每个新建的对象开启偏向锁,这样我们先让线程休眠一会就可以跳过4秒延迟,如下:

image.png

运行结果,可以看到和添加JVM,关闭延迟偏向锁-XX:BiasedLockingStartupDelay=0效果一致。

image.png

2-3-2-3、给类对象添加锁

给类对象添加锁 image.png

类对象的锁状态就是轻量级锁,如下:

image.png

2-4、偏向锁撤销

调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的,如下图

image.png

2-4-1、添加hashCode()观察偏向锁

2-4-1-1、对象处于可偏向锁调用hashCode

image.png

以上代码执行结果如下:

image.png 通过上图,obj的101(可偏向锁状态)-->通过调用hashCode-->000(轻量级锁)

2-4-1-2、对象处于偏向锁的时候

image.png 以上代码执行结果如下:

image.png 通过上图,obj的101(可偏向锁状态)-->通过synchronized加锁变革为偏向锁-->通过调用hashCode--->010(重量级锁)

2-4-1-1、小结

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向:

  • 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。

2-4-2、使用wait/notify偏向锁的状态

2-4-2-1、使用wait

在偏向锁中使用wait休眠200毫秒

image.png

运行结果如下:

image.png

通过上图,obj的101(可偏向锁状态)-->通过synchronized加锁变革为偏向锁-->通过调用obj.wait--->010(重量级锁)

2-4-2-2、使用notify

在偏向锁中添加notify

image.png

运行结果如下:

image.png 通过上图,obj的101(可偏向锁状态)-->通过synchronized加锁变更为偏向锁-->通过调用obj.notify--->000(轻量级锁)

2-4-2-3、小结

在偏向锁状态下,使用wait会启用monitor的重量级锁。使用notify会变成轻量级锁。

2-5、轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

2-5-1、轻量级锁跟踪

@Slf4j
public class LockEscalationDemo {
    public static void main(String[] args) throws InterruptedException {

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
        Thread.sleep(4000);
        Object obj = new Object();
        // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
        obj.hashCode();
        //log.debug(ClassLayout.parseInstance(obj).toPrintable());

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        + ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread1").start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj).toPrintable());
    }
}

运行结果

image.png

2-5-2、测试:锁升级场景

2-5-2-1、偏向锁升级轻量级锁

模拟两个线程轻微竞争场景

@Slf4j
public class LockEscalationDemo2 {
    public static void main(String[] args) throws InterruptedException {
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
        Thread.sleep(4000);
        Object obj = new Object();
        // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
        //obj.hashCode();
        //log.debug(ClassLayout.parseInstance(obj).toPrintable());

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName() + "开始执行。。。\n"
                        + ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj) {
                    // 思考:偏向锁执行过程中,调用hashcode会发生什么?
                    //obj.hashCode();
                    log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"
                            + ClassLayout.parseInstance(obj).toPrintable());

                }
                log.debug(Thread.currentThread().getName() + "释放锁。。。\n"
                        + ClassLayout.parseInstance(obj).toPrintable());
            }
        }, "thread1");
        thread1.start();

        //控制线程竞争时机
        Thread.sleep(1);

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        + ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread2");
        thread2.start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj).toPrintable());

    }
}

运行结果如下:

image.png

2-5-2-2、轻量级锁膨胀为重量级锁

@Slf4j
public class LockEscalationDemo3 {
    public static void main(String[] args) throws InterruptedException {

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
        Thread.sleep(4000);
        Object obj = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        + ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread2").start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj).toPrintable());
    }
}

运行结果如下:

image.png

2-6、总结:锁对象状态转换

1、不存在无锁-->偏向锁的转换
2、重量级锁为了避免park,会进行CAS自旋
3、偏向锁解锁的时候,不会变为无锁状态,而是变为可偏向锁(无线程ID)
4、偏向锁有轻微竞争会偏向锁撤销并升级为轻量级锁,轻量级锁解锁会变为无锁状态;偏向锁无锁状态,调用hashCode会变为无锁状态(偏向锁无法存hashcode,无锁状态可以存hashcode);偏向锁已锁定,同步代码块中调用hashcode或者wait会升级为重量级锁

image.png

偏向锁、无锁、轻量级锁、重量级锁的转换

image.png