【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)

一、JVM对象头

在这里插入图片描述
在JVM中,对象在内存中的布局分为三个部分:对象头、实例数据和对齐填充
HotSpot虚拟机的对象头(Object Header)包括两部分信息:
第一部分"Mark Word":用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等.
第二部分"Klass Pointer":对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 )
注意:
markword : 32位 占4字节 ,64位 占 8字节
klasspoint : 开启压缩占4字节,未开启压缩 占 8字节。

1. Klass Pointer

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

-XX:+UseCompressedOops 开启指针压缩
-XX:-UseCompressedOops 不开启指针压缩
对象头:Mark Word+Klass Pointer类型指针 未开启压缩的情况下
32位 Mark Word =4bytes ,类型指针 4bytes ,对象头=8bytes =64bits
64位 Mark Word =8bytes ,类型指针 8bytes ,对象头=16bytes=128bits;
注意:默认情况下,开启了指针压缩 可能只有12字节,必须是8字节的整数倍
所以会额外补充4个字节。

2. 实例属性

就是定义类中的成员属性

3. 对齐填充

对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

4. 查看Java对象布局

maven依赖

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

public class Test002 extends Thread {
    private DemoLock demoLock = new DemoLock();

    @Override
    public void run() {

    }

    public void create() {
        synchronized (demoLock) {

        }
    }

    public static void main(String[] args) {
        DemoLock demoLock = new DemoLock();
        System.out.println(Integer.toHexString(demoLock.hashCode()));
        System.out.println(ClassLayout.parseInstance(demoLock).toPrintable());

    }
}

5. 基本数据类型占多少字节

64位虚拟机 对象头占用16个字节— 没有压缩指针
32位虚拟机 对象头占用8个字节

1、bit --位:位是计算机中存储数据的最小单位,指二进制数中的一个位数,其值为“0”或“1”。
2、byte --字节:字节是计算机存储容量的基本单位,一个字节由8位二进制数组成。在计算机内部,一个字节可以表示一个数据,也可以表示一个英文字母,两个字节可以表示一个汉字。

1Byte = 8bit (1B=8bit)
1KB = 1024Byte(字节)=8*1024bit
1MB = 1024KB
1GB = 1024MB

数据类型
int32bit
short16bit
long64bit
byte8bit
char16bit
float32bit
double64bit
boolean1bit

5. 论证压缩效果

启用指针压缩-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops
1.默认开启指针压缩

	Object objectLock = new Object();
	System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());

在这里插入图片描述
2.关闭指针压缩
在这里插入图片描述

6. New 一个对象占用多少字节

public class Test03 {
    public static void main(String[] args) {
        DemoLockObject demoLockObject = new DemoLockObject();
        System.out.println(ClassLayout.parseInstance(demoLockObject).toPrintable());
    }

    static class DemoLockObject {
        int j = 4;
        long i = 1;
        boolean m = false;
    }
}

在开启了指针压缩的情况下:
DemoLockObject 对象头 12个字节
实例数据 int j=4 4个字节 long i=1 8个字节 boolean m=false 1个字节
对齐补充 7个字节,总共32个字节。

二、HotSpot源码分析

hotspot\src\share\vm\oops\markOop.hpp
HotSpot----阿里巴巴
在这里插入图片描述

1. 对象头详解

注意:该描述是为64位虚拟机
① 哈希值:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。它是一个地址,用于栈对堆空间中对象的引用指向,不然栈是无法找到堆中对象的

②GC分代年龄(占4位):记录幸存者区对象被GC之后的年龄age,一般age为15(阈值为15的原因是因为age只有4位最大就可以将阈值设置15)之后下一次GC就会直接进入老年代,要是还没有等到年龄为15,幸存者区就满了怎么办,那就下一次GC就将大对象或者年龄大者直接进入老年代。

③ 锁状态标志:记录一些加锁的信息(我们都是使用加锁的话,在底层是锁的对象,而不是锁的代码,锁对象的话,那会改变什么信息来表示这个对象被改变了呢?也就是怎么才算加锁了呢?
4位字符编码的最大值

如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
在这里插入图片描述

2. 获取HashCode

00010110 00000000 00000000 00000000
01110101 11010100 00011100

在这里插入图片描述

3. 对象状态

1.无锁
3.轻量锁
4.重量锁
5.GC标记
在这里插入图片描述

偏向锁标识位锁标识位锁状态存储内容
001未锁定hash code(31),年龄(4)
101偏向锁线程ID(54),时间戳(2),年龄(4)
00轻量级锁栈中锁记录的指针(64)
10重量级锁monitor的指针(64)
11GC标记空,不需要记录信息

3.1 偏向锁

偏向锁 启动jvm参数 设置:-XX:BiasedLockingStartupDelay=0
在这里插入图片描述
101----1偏向锁 锁标志位01
BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。由于这里为了测试偏向锁的性能,所以把延迟偏向锁的时间设置为0

3.2 轻量锁

public class Test04 {
    private static Object objectLock = new Object();

    public static void main(String[] args) throws InterruptedException {
        //-XX:BiasedLockingStartupDelay=0 强制开启
//        System.out.println(">>----------------无锁状态-------------------<<");
        System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (objectLock) {
                    try {
                        System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
                        Thread.sleep(5000);
                        System.out.println("..子线程..");
                    } catch (Exception e) {

                    }
                }
            }
        }, "子线程1").start();
//        Thread.sleep(1000);
//        sync();
    }

    public static void sync() throws InterruptedException {
        System.out.println(" 主线程获取锁 重量级别锁");
        //11010000 01000000
        synchronized (objectLock) {
            System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
        }
    }

}

在这里插入图片描述

3.3 重量锁

public class Test04 {
    private static Object objectLock = new Object();

    public static void main(String[] args) throws InterruptedException {
        //-XX:BiasedLockingStartupDelay=0 强制开启
//        System.out.println(">>----------------无锁状态-------------------<<");
        System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (objectLock) {
                    try {
                        System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
                        Thread.sleep(5000);
                        System.out.println("..子线程..");
                    } catch (Exception e) {

                    }
                }
            }
        }, "子线程1").start();
        Thread.sleep(1000);
        sync();
    }

    public static void sync() throws InterruptedException {
        System.out.println(" 主线程获取锁 重量级别锁");
        //11010000 01000000
        synchronized (objectLock) {
            System.out.println(ClassLayout.parseInstance(objectLock).toPrintable());
        }
    }

}

在这里插入图片描述

三、字节码文件分析

在这里插入图片描述
当我们在使用synchronized锁,通过javap 反汇编指令可以得出:
synchronized锁底层monitorenter和monitorexit指令实现。
Monitorenter:获取锁-----lock.lock();
Monitorexit:释放锁 ---- lock.unlock();
也就是底层实际上基于JVM级别C++对象
当多个线程在获取锁的时,会创建一个monitor(监视器)对象,该对象
成员变量有 owner 拥有锁的线程、recursions 重入次数等。
在这里插入图片描述
同步块的实现使用了monitorenter和monitorexit指令:他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权

有执行monitorenter 和monitorexit,其中monitorexit指令有两个,分别代表正常退出和异常退出。下面我们看看这两个指令的官方文档的介绍
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

1.1 Monitor

Hostpot 标准

1.2 Monitorenter(获取锁)

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
简单翻译:
每一个对象都会和一个监视器C++ monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:
1.若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

1.3 monitorexit

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit
简单翻译:
1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
2.执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

monitorexit释放锁:monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。为什么会有两个monitorexit,因为 Synchronized锁的同步代码块如果抛出异常的情况下,则自动释放锁。

1.4 ACC_SYNCHRONIZED

synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理

public static synchronized void count2() {
    System.out.println();
}

在这里插入图片描述

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超级码里喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值