java对象以及synchronized锁过程详解

一、java对象组成部分

java对象存储在内存中,共分为以下三个部分

1)、对象头

2)、实例数据

3)、对齐填充字节

二、对象头

java对象头有以下三部分组成:

1)、Mark Word

2)、Class Metadata Address(指向类的指针)

3)、Array Length(数组长度,只有数组对象才有)

JVM中对象头的方式有以下两种(以32位JVM为例):

1.1、普通对象:

|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

1.2、数组对象:

|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|
 

2.1、Mark Word

Mark Word 主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄、对象锁、GC等有关的信息,当这个对象被synchronized关键字作为同步锁时,围绕这个锁的一系列操作都和Mark Word有关。Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit

2.1.1、Mark Word(32位)

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

2.1.2、Mark Word(64位)

|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|

32位和64位除了位数不同外,各个部分表示的含义是一样的,其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同,如下所示:

biased_lock lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

biased_lock:对象是否启用偏向锁标记,默认启用偏向锁,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是 -XX:MaxTenuringThreshold 选项最大值为15的原因
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法 System.identityHashCode() 计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中
thread:持有偏向锁的线程ID
epoch:偏向时间戳
ptr_to_lock_record:指向栈中锁记录的指针
ptr_to_heavyweight_monitor:指向管程Monitor的指针

下面我们着重介绍一下 synchronized 同步关键字锁的实现过程,下面以 Mark Word(32位)为例,Mark Word在不同的锁状态下存储的内容不同,如下所示:

锁状态

25bit

4bit

1bit

2bit

23bit

2bit

是否偏向锁

锁标志位

无锁

对象的HashCode

分代年龄

0

01

偏向锁

线程ID

Epoch

分代年龄

1

01

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向重量级锁的指针

10

GC标记

11

JDK1.6以后的版本在处理同步锁时加入了锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁

synchronized 流程

JVM synchronized  锁通过以下流程来修改 Mark Word 的,整个锁的流程用下面一张图概括为:

2.1.3、默认开启偏向锁

1)、当对象没有被加锁时,即普通对象,则偏向锁状态为 1,锁标志位为 01,线程ID为0,如下所示:

2)、当对象被加锁并有一个线程A抢到了锁时,偏向锁状态位和锁标志位都不变,只是将前 23bit 记录抢到锁的线程id记录到线程ID中,对象进入偏向锁状态,如下所示:

当没有其他线程竞争该对象锁时,线程A释放锁后,对象会继续保持偏向锁状态,即 步骤 2 的状态,线程ID还是维持线程A的ID

3)、当线程A再次来获取锁时,JVM发现对象处于偏向锁状态,即 锁标志位是01,偏向锁标志也是 1,而且又是同一个线程,则只需要将获取锁的次数加1即可,无需重新获取锁,直接执行代码逻辑

4)、当线程B来获取锁时,JVM发现对象处于偏向锁状态,线程ID记录的不是线程B,则线程B 会通过CAS 自旋获取锁,如果 B 获取锁成功,则对象处于偏向锁状态,线程ID 修改为线程 B 的id,否则执行步骤5

5)、由于B没有获取到锁,说明锁竞争比较激烈,此时偏向锁会升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针,上述两个保存操作都是CAS操作,如果保存成功,则表示获取锁成功,锁标志位修改为 00,此时对象进入轻量级锁状态,如下所示:

6)、如果 B 获取锁失败,即表示锁升级为轻量级锁失败,此时锁会膨胀为重量级锁,锁的标志位修改为 10,此后没有获取到锁的线程全部会处于阻塞状态,不会自旋获取锁。JVM会单独创建一个 ObjectMonitor 对象监视器,Mark Word 指针指向该监视器,所有阻塞的线程会加入链表中排队等待获取 ObjectMonitor 对象锁(即 monitorenter、monitorexit 逻辑),如下所示:

一旦对象锁膨胀为重量级锁后,即使所有线程释放锁后,对象锁也不会恢复到无锁或者偏向锁状态了,此后所有线程的加锁操作都是重量级锁过程

2.1.4、禁用偏向锁(-XX:-UseBiasedLocking

1)、当对象没有被加锁时,即普通对象,则偏向锁状态为 0,锁标志位为 01,对象的hashCode值,如下所示:

2)、当对象被加锁并有一个线程A抢到了锁时,JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针,上述两个保存操作都是CAS操作,如果保存成功,则表示获取锁成功,锁标志位修改为 00,此时对象进入轻量级锁状态

当没有其他线程竞争该对象锁时,线程A释放锁后,对象会重新恢复到无锁状态,即 步骤 1 的状态

3)、当线程A再次来获取锁时,JVM发现对象如果处于无锁状态时,则按照步骤2执行,否则(重入锁)只需要将获取锁的次数加1即可,无需重新获取锁,直接执行代码逻辑

4)、当线程B来获取锁时,JVM发现对象处于轻量级锁状态,JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针,上述两个保存操作都是CAS操作,如果保存成功,则表示获取锁成功,锁标志位修改为 00,此时对象进入轻量级锁状态,如下所示:

5)、如果 B 获取锁失败,此时锁会膨胀为重量级锁,锁的标志位修改为 10,此后没有获取到锁的线程全部会处于阻塞状态。JVM会单独创建一个 ObjectMonitor 对象监视器,Mark Word 指针指向该监视器,所有阻塞的线程会加入链表中排队等待获取 ObjectMonitor 对象锁(即 monitorenter、monitorexit 逻辑),如下所示:

一旦对象锁膨胀为重量级锁后,即使所有线程释放锁后,对象锁也不会恢复到无锁或者偏向锁状态了,此后所有线程的加锁操作都是重量级锁过程

2.1.5  java 输出对象信息

1)、maven 引入 jol-core jar 包,如下所示:

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.13</version>
        </dependency>

2)、示例demo:

/**
 * synchronized 测试
 *
 * -XX:-UseBiasedLocking 禁止偏向锁,默认偏向锁是开启的
 *
 * @author supu
 * @since 2020-08-31 11:00
 **/
public class SynchronizedDemo {

    public static void main(String[] args) {
        System.out.println(VM.current().details());

        A a = new A();
        a.setAge(10);
        a.setName("zs");

        ClassLayout c2 = ClassLayout.parseInstance(a);

        System.out.println(c2.toPrintable());

        new Thread(() -> {
            synchronized (a){
                System.out.println("*********** a 第一次被锁定后的对象头 markword ***********");
                System.out.println(c2.toPrintable());


                /*synchronized (a){
                    System.out.println("*********** a 第二次被锁定后的对象头 markword ***********");
                    System.out.println(c2.toPrintable());
                }*/

                try {
                    TimeUnit.MILLISECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        try {
            TimeUnit.MILLISECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 当多个线程竞争锁时,偏向锁升级为轻量级锁还是重量级锁的条件时,后者获取锁的线程是否能在指定时间内自旋CAS获取到锁,
        // 如果获取到锁了则升级为轻量级锁,否则升级为重量级锁

        synchronized (a){
            System.out.println("*********** a 第二次被锁定后的对象头 markword ***********");
            System.out.println(c2.toPrintable());
        }

        System.out.println("************ a 释放锁定后的对象头 markword **********");
        System.out.println(c2.toPrintable());

    }

    static class A {
        private int age;
        private String name;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            A a = (A) o;
            return age == a.age &&
                    Objects.equals(name, a.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(age, name);
        }
    }
}

2.2、Class Metadata Address(指向类的指针)

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

1)、每个Class的静态变量属性指针(即静态变量)

2)、每个对象的成员变量属性指针(即成员变量)

3)、普通对象数组的每个元素指针

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

2.3、Array Length(数组长度,只有数组对象才有)

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

三、实例数据

对象的实例数据就是在java代码中能看到的属性和他们的值

四、对齐填充字节

因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,除此之外没有特别的功能

五、参考

https://wiki.openjdk.java.net/display/HotSpot/Synchronization

https://www.jianshu.com/p/3d38cba67f8b

猜你喜欢

转载自blog.csdn.net/ywlmsm1224811/article/details/108322059