本文分析的问题:

  1. synchronized 字节码文件分析之 monitorenter、monitorexit 指令

  2. 为什么任何一个Java对象都可以成为一把锁?

  3. 对象的内存结构

  4. 锁升级过程

  5. Monitor 是什么、源码查看

字节码分析

synchronized的3种使用方式

  1. 作用于实例方法,对对象加锁

  2. 作用于静态方法,对类加锁

  3. 作用于代码块,对 () 里的对象加锁

先说结论:通过 monitorenter、monitorexit 指令来做

synchronized 关键字底层原理属于 JVM 层面的东西。

代码块

monitorenter、monitorexit 指令来做

代码:

public void m1(){
synchronized (this){ }
}

编译后使用 javap -v xxx.class 命令查看:

  • 一般情况下就是 1 个 monitorenter 对应 2 个 monitorexit

    • 正常处理正常释放时有一个 monitorexit,考虑到有异常时,锁应该也要被释放,所以也会有一个 monitorexit
  • 极端情况下:如果方法里抛出异常了,就只会有一个 monitorexit 指令

包含一个 monitorenter 指令以及两个 monitorexit 指令,这是为了保证锁在同步代码块正常执行以及出现异常

的这两种情况下都能被正确释放

异常情况下:只有一个 monitorexit 指令

实例方法

ACC_SYNCHRONIZED 这个标识来做

代码:

public synchronized void m1(){

}

结果:在方法下没有那两个指令,取而代之的是 ACC_SYNCHRONIZED 这个标识。该标识指明了该方法是一个同步方法

JVM通过该 ACC_SYNCHRONIZED 标识来辨别一个方法是否是一个同步方法,从而执行相应的同步调用

静态方法

ACC_SYNCHRONIZED这个标识来做

代码:

public static synchronized void m1(){

}

结果:可以看到还是通过 ACC_SYNCHRONIZED 这个标识来做。

ACC_STATIC 这个标识是用来区分实例方法和静态方法的,就算不加 synchronized 也会有。

对象内存结构

为了可以更加直观的看到对象结构,我们可以借助 openjdk 提供的 JOL 工具进行分析。

JOL分析工具

JOL(Java 对象布局)用于分析对象在JVM的大小和分布

官网:https://openjdk.org/projects/code-tools/jol/

  <!--
https://mvnrepository.com/artifact/org.openjdk.jol/jol-cli
定位:分析对象在JVM的大小和分布
-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-cli</artifactId>
<version>0.14</version>
</dependency>

对象内存结构

可以看到总共分为三部分:对象头、实例数据、对齐填充

对象头

在64位系统中,Mark Word 占了 8 个字节,类型指针占了 8 个字节,一共是 16 个字节

Mark Word

MarkWord 中存了一些信息:hashCode的值、gc相关、锁相关

具体结构

  • unused:未使用的位置

  • hashcode:hashcode 的值

  • age:GC年龄(4位最大只能表示15)

  • biased_lock:是否是偏向锁(0不是;1是)

  • thread:线程id

  • epoch:偏向时间戳

  • ptr_to_lock_record:存储指向栈帧中的锁记录(LockRecord)的指针

  • ptr_to_heavyweight_monitor:指向重量级锁的指针(也就是指向 ObjectMonitor 的指针)

  • 锁标志位:01 代表有锁;00 代表偏向锁;10 代表轻量级锁;11 代表重量级锁

markOop.hpp 中的 C++ 源码查看:

//  32 bits:  32位的
// --------
// 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: 64位的
// --------
// 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)

源码地址:https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/objectMonitor.hpp

Class Pointer

对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象哪个类的实例

我们怎么知道创建的这个对象是什么类型的,就通过这个指针指向方法区的类元信息(kclass pointer)。

可能会进行指针压缩。

实例数据

存放类的属性(Field)数据信息,包括父类的属性信息

对齐填充

虚拟机要求对象起始地址必须是 8 字节的整数倍,所以填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按 8 字节补充对齐

比如:

  • 只有一个类的话,类里面是空的那就是 16 字节 = MarkWord + 类型指针(不考虑指针压缩的情况下)。这时不需要对齐填充来对齐,因为 16 字节本身就是 8 的整数倍。
  • 但假如此时有了属性,int = 4字节,boolean = 1 字节,加起来 = 16+4+1 = 21 字节,这时就不是 8 的整数 倍的,这时就需要对齐填充来补齐了
class Person {
int age;
boolean isFlag;
}

使用JOL工具证明

简单使用:

public static void main(String[] args) {
// 获取JVM详细信息
System.out.println(VM.current().details());
// 对象头大小 开启指针压缩是12=MarkWord(8)+ClassPointer(4),没开启是16=MarkWord(8)+ClassPointer(8)
System.out.println(VM.current().objectHeaderSize());
// 对齐填充 为什么都是8的倍数?
System.out.println(VM.current().objectAlignment());
}

验证对象内存结构

代码:

public static void main(String[] args) {
Object obj = new Object();
System.out.println(obj + " 十六进制哈希:" + Integer.toHexString(obj.hashCode()));
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

结果:

  • Object对象,总共占16字节
  • 对象头占 12 个字节,其中:mark-word 占 8 字节、Klass Point 占 4 字节
  • 最后 4 字节,用于数据填充对齐
指针压缩

不是说类型指针是8字节吗,到这里怎么变为4字节了?

那是因为被 指针压缩 了(开启后性能会更好)

命令查看: java -XX:+PrintCommandLineFlags -version

这个参数就是压缩指针的参数:-XX:+UseCompressedClassPointers +代表开启,- 代表关闭

关闭后,运行后,再次查看:-XX:-UseCompressedClassPointers

这次就没有对齐填充了。

锁升级过程

无锁 ---> 偏向锁 ---> 轻量级锁 -> 重量级锁

出现的背景

之前 synchronized 是重量级锁,依靠 Monitor 机制实现。

Monitor 是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的线程同步。这种机制需要用户态和内核态之

间来切换。但Mutex是系统方法,由于权限的关系,应用程序调用系统方法时需要切换到内核态来执行。

所以为了是减少用户态和内核态之间的切换。因为这两种状态之间的切换的开销比较高。

先来一个 狗 的类,用来创建对象

class Dog {

}

无锁

创建一个对象,没有一个线程来占有它,这时就是无锁

无锁时 Mark Word 的结构:

| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01    |       Normal	      |

测试代码:

public static void main(String[] args) {
Dog dog = new Dog();
dog.hashCode();
System.out.println(ClassLayout.parseInstance(dog).toPrintable()); System.out.println("======================================================");
System.out.println(dog);
System.out.println("十六进制:" + Integer.toHexString(dog.hashCode()));
System.out.println("二进制:" + Integer.toBinaryString(dog.hashCode()));
}

对照上面的结构来看:从后往前按照 Mark Word 结构来看。每8bit从前往后看。

偏向锁

当第一个线程来获取到它时(没有竞争/一次就竞争成功),这时它就是偏向锁,只偏向于这一个线程(CAS)

偏向锁、轻量级锁的 Mark Word 的结构:

|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|

测试代码:

public static void main(String[] args) {
Dog dog = new Dog(); synchronized (dog){
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}

结果:

这里的锁为什么直接是轻量级锁呢?

因为偏向锁默认是延迟开启的,所以进入了轻量级锁状态。

使用 java -XX:+PrintFlagsInitial | grep BiasedLock 命令在 Git Bash 下执行:

所以需要添加参数 -XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。或者让程序睡了 5 秒后再执行。

添加参数/睡5秒后执行,发现偏向锁出现了:

1 代表是偏向锁,01 代表有锁

轻量级锁

其实就是自旋锁(底层是CAS)

其它线程来竞争锁,并且竞争失败,会到一个全局安全点来把这个锁升级为轻量级锁

轻量级锁的 Mark Word 结构:

|             ptr_to_lock_record:62                          | 00    | Lightweight Locked |

测试代码:通过调用 hashCode() 来得到轻量级锁(因为偏向锁里没有地方存储 hashCode 的值)

public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
dog.hashCode();
synchronized (dog) {
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}

结果:

自旋次数

JDK6之前

  • 默认启用,默认情况下自旋的次数是10次,或者自旋线程数超过CPU核数一半

JDK6之后 自适应自旋锁

  • 线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。

  • 反之,如果很少会自旋成功,那么下次会减少自旋的次数其至不自旋,避免CPU空转。

  • 自适应意味着自旋的次数不是固定不变的,而是根据:同一个锁上一次自旋的时间。拥有锁线程的状态来决定。

重量级锁

自旋到一定次数后,还没获取到锁,会将其升级为重量级锁。那就是阻塞了,用户态和内核态之间的切换

基于 Monitor 的实现,monitorenter 和 monitorexit 指令来实现

重量级锁的 Mark Word 的结构:

|       	  ptr_to_heavyweight_monitor:62                    | 10	   | Heavyweight Locked |

测试代码:


结果:

锁升级后,hash值去哪?

  • 无锁:就存在 Mark Word 里

  • 偏向锁:没有地方存 hash 值了

    • 如果在 synchronized 前调用了 hashCOde() ,此时偏向锁会升级为轻量级锁

    • 如果在 synchronized 中调用了 hashCode() ,此时偏向锁会升级为重量级锁

  • 轻量级锁:栈帧中的锁记录(Lock Record)里

  • 重量级锁:Mark Word 保存重量级锁的指针,底层实现 ObjectMonitor 类里有字段记录加锁状态的 Mark Word 信息

必须说的 monitor

在 HotSpot 虚拟机中,monitor 是由 C++ 中的 ObjectMonitor 实现。

什么是 Monitor

Monitor 是管程,是同步监视器,是一种同步机制。为了保证数据的安全性

Monitor 有什么用

提供了一种互斥机制。限制同一时刻只能有一个线程进入 Monitor 的临界区,保护数据安全。

用于保护共享数据,避免多线程并发访问导致数据不一致。

synchronized 的重量级锁就是用 Monitor 来实现的

Monitor 的源码分析

每个 Java 对象都自带了一个 monitor 对象,所以每个 Java 对象都可以成为锁。

源码:Java 中的每个对象都继承自 Object 类,而每个 Java 对象在 JVM 内部都有一个 C++ 对象 oopDesc 与其对应,而对应的 oopDesc 内有一个属性是 markOopDesc 对象(这个对象就是 Java 里的 Mark Word),这个 markOopDesc 内有一个 monitor() 方法返回了 ObjectMonitor 对象(hotspot中,这个对象实现了 monitor )

翻译过来就是:Java 中的每个对象都继承自 Object 类,oopDesc 是每个 Java 对象的顶层父类,这个父类内有个属性是 markOopDesc 对象,也就是对象头。这个对象头是存储锁的地方,里面有一个 ObjectMonitor。

每一个被锁住的对象又都会和 Monitor 关联起来,通过对象头里的指针。

oopDesc

这个类是每个 Java 对象的基类。每个 Java 对象在虚拟机内部都会继承这个 C++ 对象。

源码地址:https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/oop.hpp

markOopDesc

这个类也是 oopDesc 的子类。这个类就是 Mark Word 对象头。

里面有一个 monitor() 方法返回了 ObjectMonitor 对象。

monitor():

源码地址:https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/markOop.hpp

ObjectMonitor

在 hotspot 虚拟机中,ObjectMonitor 是 Monitor 的实现。

源码地址:https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/objectMonitor.hpp

为什么任何一个Java对象都可以成为一把锁

Java 中的每个对象都继承自 Object 类,虚拟机源码中 oopDesc 是每个 Java 对象的顶层父类,这个父类内有个属性是 markOopDesc 对象,也就是对象头。这个对象头是存储锁的地方,里面有一个 ObjectMonitor 。而 monitor 的实现就是 ObjectMonitor 对象监视器。

参考资料

大部分参考:

  1. https://www.bilibili.com/video/BV1ar4y1x727/

Mark Word 代码块里的结构参考视频的笔记(图为自画):

  1. https://www.bilibili.com/video/BV16J411h7Rd

少部分参考:

  1. https://www.cnblogs.com/wuzhenzhao/p/10250801.html

  2. https://segmentfault.com/a/1190000037645482

  3. https://www.cnblogs.com/mic112/p/16388456.html

oopDesc、markOopDesc 和 Java 对象之间的关系参考:

  1. https://www.cnblogs.com/mazhimazhi/p/13289686.html

  2. https://blog.csdn.net/qq_31865983/article/details/99173570

openjdk 源码位置参考:里面也有 ObjectMonitor 原理

  1. https://www.cnblogs.com/webor2006/p/11442551.html

synchronized原理-字节码分析、对象内存结构、锁升级过程、Monitor的更多相关文章

  1. Synchronized加锁、锁升级和java对象内存结构

    首先了解一下JMM中定义的内存操作: 一个线程操作数据时候都是从主内存(堆内存)读取到自己工作内存(线程私有的数据区域)中再进行操作.对于硬件内存来说,并没有工作内存和主内存的区分,这都是java内存 ...

  2. Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

    前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析. 一.问题引出 先看一段代码 packa ...

  3. v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式 | 百篇博客分析OpenHarmony源码

    百篇博客分析|本篇为:(共享内存篇) | 进程间最快通讯方式 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁) | 同样 ...

  4. Memcached源码分析之内存管理

    先再说明一下,我本次分析的memcached版本是1.4.20,有些旧的版本关于内存管理的机制和数据结构与1.4.20有一定的差异(本文中会提到). 一)模型分析在开始解剖memcached关于内存管 ...

  5. ffplay源码分析2-数据结构

    ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg提供的解码器和SDL库进行视频播放.本文基于FFmpeg工程4.1版本进行分析,其中ffplay源码清单如下: https://gith ...

  6. JAVA 对象内存结构

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

  7. JVM-String比较-字节码分析

    一道String字符串比较问题引发的字节码分析 public class a { public static void main(String[] args)throws Exception{ } p ...

  8. 并发编程(四)—— ThreadLocal源码分析及内存泄露预防

    今天我们一起探讨下ThreadLocal的实现原理和源码分析.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两 ...

  9. Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...

  10. Kubernetes Job Controller 原理和源码分析(二)

    概述程序入口Job controller 的创建Controller 对象NewController()podControlEventHandlerJob AddFunc DeleteFuncJob ...

随机推荐

  1. #树上差分 or 01-Trie#洛谷 6623 [省选联考 2020 A 卷] 树

    题目 分析(01trie) 考虑用trie做需要满足什么操作:加入某个数.01-Trie的合并.全局加一. 主要是全局加一比较难做,考虑改变的地方就是 \(X*2^T+2^T-1\). 把01-Tri ...

  2. C++ 编程必备:对象生命周期管理的最佳实践

    在C++中,对象的生命周期是指对象存在的时间段,从对象创建到对象销毁的整个过程.正确地管理对象的生命周期是编写高效.可靠C++代码的关键之一 对象的创建 在C++中,对象可以通过三种方式创建:静态分配 ...

  3. SQL(Structured Query Language)简介和常见 SQL 命令示例

    简介 SQL(Structured Query Language)是一种用于访问和操作关系型数据库的标准语言.它是一个功能强大的语言,用于执行各种数据库操作,包括检索数据.插入新记录.更新记录.删除记 ...

  4. Seaborn调色盘设置

    调色盘设置 设置调色 color_palette()设置调色盘,返回一个调色盘的颜色列表,默认6种颜色:deep, muted, bright, pastel, dark, colorblind. s ...

  5. Ubuntu SVN 需要证书及密码验证问题

    问题概览 问题一 Ubuntu 20.04 下使用 SVN ,会报错 SVN 的证书错误,无论是选择接受 t 还是永久接受 p,下次都会要求再次接受:在 kali 或者 Windows 上没有出现该问 ...

  6. Hive 查看表/分区更新时间

    1.查看分区 hive> show partitions table_name; 2.查看分区更新时间 获取hdfs路径 hive> desc formatted table_name; ...

  7. css 文字溢出省略号

    前言 css 文字溢出后显示省略号,这是一个非常常规的操作,但是你会发现在网上很多给出的例子两行之后显示省略号,却没有用. 这是为什么呢?please look follow. 正文 在一行省略的: ...

  8. jenkins 持续集成和交付 —— 参数化构建(八)

    前言 这个其实就是我们构建的脚本时候希望能有一些变量,能给我们更多的选择. 正文 选择这个: 填好后,这边就有一个参数配置: 那么接下来就是把变量放到我们的脚本中. 如下: 脚本变成变量符,这样就ok ...

  9. Mysql安装和远程登录--Centos7

    在Centos7中使用的包管理工具是yum,当然使用包管理工具安装也是最方便的. 本文操作内容需要在root用户下,否则有些步骤无法成功执行. 系统环境信息展示 安装 MySQL 提供的 RPM wg ...

  10. 力扣20(java)-有效的括号(简单)

    题目: 给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效. 有效字符串需满足: 左括号必须用相同类型的右括号闭合.左括号必须以正确的顺序闭合.每个右括 ...