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

目录

  • 一、JVM对象头
    • 1. Klass Pointer
    • 2. 实例属性
    • 3. 对齐填充
    • 4. 查看Java对象布局
    • 5. 基本数据类型占多少字节
    • 5. 论证压缩效果
    • 6. New 一个对象占用多少字节
  • 二、HotSpot源码分析
    • 1. 对象头详解
    • 2. 获取HashCode
    • 3. 对象状态
      • 3.1 偏向锁
      • 3.2 轻量锁
      • 3.3 重量锁
  • 三、字节码文件分析
    • 1.1 Monitor
    • 1.2 Monitorenter(获取锁)
    • 1.3 monitorexit
    • 1.4 ACC_SYNCHRONIZED

一、JVM对象头

【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第1张图片
在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.jolgroupId>
    <artifactId>jol-coreartifactId>
    <version>0.10version>
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

数据类型
int 32bit
short 16bit
long 64bit
byte 8bit
char 16bit
float 32bit
double 64bit
boolean 1bit

5. 论证压缩效果

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

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

【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第2张图片
2.关闭指针压缩
【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第3张图片

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----阿里巴巴
【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第4张图片

1. 对象头详解

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

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

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

如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第5张图片

2. 获取HashCode

00010110 00000000 00000000 00000000
01110101 11010100 00011100

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

3. 对象状态

1.无锁
3.轻量锁
4.重量锁
5.GC标记
【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第7张图片

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

3.1 偏向锁

偏向锁 启动jvm参数 设置:-XX:BiasedLockingStartupDelay=0
【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第8张图片
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());
        }
    }

}

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

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());
        }
    }

}

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

三、字节码文件分析

【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第11张图片
当我们在使用synchronized锁,通过javap 反汇编指令可以得出:
synchronized锁底层monitorenter和monitorexit指令实现。
Monitorenter:获取锁-----lock.lock();
Monitorexit:释放锁 ---- lock.unlock();
也就是底层实际上基于JVM级别C++对象
当多个线程在获取锁的时,会创建一个monitor(监视器)对象,该对象
成员变量有 owner 拥有锁的线程、recursions 重入次数等。
【JUC并发编程】synchronized原理分析(中)(JVM对象头/ HotSpot源码分析/ 字节码文件分析)_第12张图片
同步块的实现使用了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();
}

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

你可能感兴趣的:(JUC并发编程,JUC,synchronized,JVM对象头,HotSpot源码分析,Monitor)