Java 对象结构与锁实现原理以及 MarkWord 详解
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
状态 | 标志位 | 存储内容 |
---|---|---|
未锁定 | 01 | 对象哈希码、对象分代年龄 |
轻量级锁定 | 00 | 指向锁记录的指针 |
重量级锁定 | 10 | 执行重量级锁定的指针 |
GC 标记 | 11 | 空(不需要记录信息) |
可偏向 | 01 | 偏向线程的ID、偏向时间戳、对象分代年龄 |
jvm如何使用锁和Mark Word?
- 没有被锁时,就是一个普通对象,Mark Word 记录对象的 HashCode,锁标志位是 01 是否偏向位 0;
- 当对象被当做同步锁并且有一个线程抢到了锁,锁标志位还是 01,但是否偏向位是1,前面23bit 保存抢到锁的线程id,表示进入偏向状态。
- 当线程 A 再次试图获取锁时, JVM 发现同步锁对象的标志位是 01,是否偏向也是1,并且记录中线程id 也是线程 A 自己的 id ,可以直接执行同步代码块。
- 当线程 B 试图获取这个锁时,JVM发现同步锁处于偏向状态,但是mark word 中线程 id 记录不是自己,会有一个锁升级(轻量级锁),两个线程公平竞争,先抢到的线程占有锁对象并执行代码,锁对象的 Mark Word 就执行哪个线程的栈帧中的锁记录。(升级轻量级锁时,JVM 会在当前线程的线程栈中开辟一块单独的空间,里面保存对象的Mark Word 的副本,同时在对象锁Mark Word 中保存指向这片空间的指针。)这里的操作都是 CAS ,如果保存成功,则表示抢到了同步锁,把 MarkWord 的标志位改成 00. 可以执行同步代码块。如果执行失败,执行步骤5 。
- 轻量级锁抢锁失败,JVM 会使用自旋锁,自旋锁并不是一个锁状态。只是代表不断的重试(自旋的次数由 JVM 控制,依据版本的不同次数也不相同),尝试抢锁。
- 自旋锁重试之后依然没有抢到锁,升级为重量级锁。标志位改成 10 。这个状态下,未抢到锁的线程都会被阻塞。
指针压缩
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项 +UseCompressedOops 开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
-
每个Class的属性指针(即静态变量)
-
每个对象的属性指针(即对象变量) 普通对象数组的每个元素指针
-
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向 PermGen 的 Class 对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。