论坛
潜水/灌水快乐,沉淀知识,认识更多同行。
ToB圈子
加入IT圈,遇到更多同好之人。
朋友圈
看朋友圈动态,了解ToB世界。
ToB门户
了解全球最新的ToB事件
博客
Blog
排行榜
Ranklist
文库
业界最专业的IT文库,上传资料也可以赚钱
下载
分享
Share
导读
Guide
相册
Album
记录
Doing
搜索
本版
文章
帖子
ToB圈子
用户
免费入驻
产品入驻
解决方案入驻
公司入驻
案例入驻
登录
·
注册
只需一步,快速开始
账号登录
立即注册
找回密码
用户名
Email
自动登录
找回密码
密码
登录
立即注册
首页
找靠谱产品
找解决方案
找靠谱公司
找案例
找对的人
专家智库
悬赏任务
圈子
SAAS
ToB企服应用市场:ToB评测及商务社交产业平台
»
论坛
›
软件与程序人生
›
后端开发
›
Java
›
synchronized原理-字节码分析、对象内存布局、锁升级过 ...
synchronized原理-字节码分析、对象内存布局、锁升级过程、Monitor ...
饭宝
金牌会员
|
2024-5-19 16:56:31
|
显示全部楼层
|
阅读模式
楼主
主题
905
|
帖子
905
|
积分
2715
本文分析的问题:
synchronized 字节码文件分析之 monitorenter、monitorexit 指令
为什么任何一个Java对象都可以成为一把锁?
对象的内存布局
锁升级过程
Monitor 是什么、源码查看
字节码分析
synchronized的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/
<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 |
复制代码
测试代码:
[code][/code]结果:
锁升级后,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 对象监视器。
参考资料
大部分参考:
https://www.bilibili.com/video/BV1ar4y1x727/
Mark Word 代码块里的布局参考视频的笔记(图为自画):
https://www.bilibili.com/video/BV16J411h7Rd
少部分参考:
https://www.cnblogs.com/wuzhenzhao/p/10250801.html
https://segmentfault.com/a/1190000037645482
https://www.cnblogs.com/mic112/p/16388456.html
oopDesc、markOopDesc 和 Java 对象之间的关系参考:
https://www.cnblogs.com/mazhimazhi/p/13289686.html
https://blog.csdn.net/qq_31865983/article/details/99173570
openjdk 源码位置参考:内里也有 ObjectMonitor 原理
https://www.cnblogs.com/webor2006/p/11442551.html
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
本帖子中包含更多资源
您需要
登录
才可以下载或查看,没有账号?
立即注册
x
回复
使用道具
举报
0 个回复
正序浏览
返回列表
快速回复
高级模式
B
Color
Image
Link
Quote
Code
Smilies
您需要登录后才可以回帖
登录
or
立即注册
本版积分规则
发表回复
回帖并转播
发新帖
回复
饭宝
金牌会员
这个人很懒什么都没写!
楼主热帖
53基于java的资源博客论坛系统设计与实 ...
zotero+坚果云实现多pc端及iPad同步管 ...
天涯神贴合集500篇(2023最新) ...
Android——一个简单的记账本APP ...
需求:清空三个月前的操作日志,并生成 ...
nginx 常用指令配置总结
【分布式计算】学习笔记(期末复习) ...
PerfView专题 (第十一篇):使用 Diff ...
SQL函数-聚合函数
面试官:@Configuration 和 @Component ...
标签云
存储
挺好的
服务器
快速回复
返回顶部
返回列表