在上一篇文章中我们讲了Synchronized关键字的底层实现第一部分,主要涉及JVM的指令和Monitor对象。今天我们就继续来挖掘一下 Synchronized的底层实现,围绕对象的存储结构和对象头的构成和作用进行一下解析。
本文主要围绕下面两个问题进行解析。
- 对象的存储结构了解吗?
- 对象头里有什么?作用呢?
1. JVM中对象的存储结构
我们以Hotspot为例,下图描述了Java对象实例在JVM中的存储结构。
对象头:
-
- Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit)。
- 特殊情况,对象是数组类型,额外需要4个字节(会多占一个机器码),用来记录数组长度(因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小)。
Hotspot虚拟机的对象头(非数组情况下)主要包括两部分数据:
- Mark Word(标记字段)
- Class Pointer(类型指针)
Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,64位时8字节,开启指针压缩或者最大堆内存小于32G时为4个字节。
对象头中的MarkWord:
JVM在1.6版本以上默认开启指针压缩,所以会压缩到4个字节(原来8个字节),32bit。
markWord中储存的内容:
-
-
- 对象哈希码、对象分代年龄
- 指向锁记录的指针
- 指向重量级锁的指针
- 空,不需要记录信息
- 偏向线程 ID、偏向时间截、对象分代年龄
-
我们这里简单提一下锁的四种不可逆的状态:无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态
下图是64位虚拟机在不压缩情况下的展示,也就是25bit扩充为57bit
instance data 实例数据:
在对象实例的存储结构中,对象头之后的,就是用来存放类的属性数据信息的instance data,其中包括父类的属性信息等内容。
padding 对齐填充:
存储结构中最后部分的对齐填充,主要由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;(比如:7个Long)
四种不可逆的状态:无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态
下表来自《Java并发编程的艺术》:
锁 |
优点 |
缺点 |
适用场景 |
偏向锁 |
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 |
如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 |
适用于只有一个线程访问同步块场景。 |
轻量级锁 |
竞争的线程不会阻塞,提高了程序的响应速度。 |
如果始终得不到锁竞争的线程使用自旋会消耗CPU。 |
追求响应时间。同步块执行速度非常快。 |
重量级锁 |
线程竞争不使用自旋,不会消耗CPU。 |
线程阻塞,响应时间缓慢。 |
追求吞吐量。同步块执行时间较长。 |
更多更精彩的内容请关注。
V信公众号搜索 “程序员一棵树”。 内有相关文档原文和免费资料。
D音搜索 “程序员一棵树”。
B站搜索 “程序员一棵树”。