锁原理
通过openJDK 开发的jar包-JOL去分析Java对象头信息
首先Java对象有三部分组成 1:对象头,2:对象的实例数据,3:对齐字节padding
对象的实例数据:对象中的字段数据
对齐字节(padding)
对象头:每个gc管理的堆对象开头的公共结构(每个oop都指向一个对象标头)包括堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息,
由两个词组成(mark word,klass word)。64位系统为例,一共占128bit 16byte,开启指针压缩时占12byte(klass word 有关)。
klass word
klass word为对象头的第二个词,主要指向对象的元数据,也可以说是指向类元数据的指针。元数据可以理解为Class对象模板
开启指针压缩的时候占4byte(32bit),不开启占8byte(64bit)
mark word
mark word为第一个word根据文档可以知他里面包含了锁的信息,hashcode,gc信息等等 总长度64位(bit) 8byte,
对象不同状态(锁的各种状态,GC标记)时,对象头中每个位的数据也不同。详见下表
|———————————————————————————————————————————————————————————————————————————————————————————————————————————————|
| Object header |
| ——————————————————————————————————————————————————————————————————————————————————————————————————————————————|
|状态 | mark word 8Byte | klass word |
|——————|———————————————————————————————————————————————————————————————————————————————|————————————————————————|
|无锁 |unused:25 | identity_hashCode:31 | unused 1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object|
|——————|———————————————————————————————————————————————————————————————————————————————|————————————————————————|
|偏向锁|thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object|
|——————|———————————————————————————————————————————————————————————————————————————————|————————————————————————|
|轻量锁| ptr_to_lock_record:62 | lock:2 | OOP to metadata object|
|——————|———————————————————————————————————————————————————————————————————————————————|————————————————————————|
|重量锁| ptr_to_heavyweight_monitor 62 | lock:2 | OOP to metadata object|
|——————|———————————————————————————————————————————————————————————————————————————————|————————————————————————|
|GC标识| | lock:2 | OOP to metadata object|
|——————————————————————————————————————————————————————————————————————————————————————|—————————————————————————
文字解读上表mark word信息:
对象状态无锁状态时,前0-24位未使用,25-55位hashcod,56位未使用,57-60位对象GC状态年龄代标识,61位偏向锁标记, 62-63锁状态,
对象状态偏向锁时,前0-53位为线程标记,54-55位为 epoch,56位未使用,57-60位对象GC状态年龄代标识,61位偏向锁标记,62-63锁状态,
对象状态轻量级锁时,0-61位为指针 ptr指针,指向lock的record指针,62-63锁状态
对象状态为重量级锁时,0-61位为xxx ptr指针,指向monitor对象的指针,62-63锁状态
对象状态被GC标识时 0-61为空,
通过将第61、62、63三个位的数值组合,便可以标识五种对象状态即 000:轻量级,001:无锁,101:偏向锁,010:重量级锁,空+11:GC标记
epoch:类似的机制称为批量再偏置(bulk rebiasing),它可以优化这样的情况:一个类的对象被不同的线程锁定和解锁,但是不会并发。
它在不禁用偏向锁定的情况下使类的所有实例的偏向无效。类中的epoch值充当表示偏差有效性的时间戳。此值将在对象分配时复制到标题字中。
然后,可以有效地在适当的类中以历元增量的形式实现批量再偏。下一次这个类的实例被锁定时,代码将检测到一个不同的
只有调用了对象的hashCode后,对象头信息mark word 词中才会有hashCode的信息,否则mark word中将不会有hashCode信息,通过JOL去打
印类的布局,可以查看到对象头信息再调用了hashCode方法后的变化。小端存储模型下,对象的hashCode高位对应mark word中存储hashCode
值得低位。比如,对象调用了hashCode方法后,hashCode值十六进制下为198e2867,前文提到mark word 中前56位为hashCode ,小段存储模式下,
后56位对应hashCode得值,即:00000001 01100111(67) 00101000(28) 10001110(8e) 00011001(19) 00000000 00000000 00000000
运行一下代码:
package com.test;
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample2 {
//Thread.sleep(5000);
static A a;
public static void main(String[] args) throws Exception {
//Thread.sleep(5000);
a = new A();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
public static void sync() throws InterruptedException {
synchronized (a){
System.out.println("我也不知道要打印什么");
}
}
}
理应输出的时偏向锁,然后通过JOL打印出来的却是轻量级锁,如果打开注释的Thread.sleep(5000);则运行结果会打印此对象状态位偏向锁,这是因为
JVM在启动时做的偏向锁延迟导致的。可以通过JVM参数 :-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0禁止延迟偏向锁。
为什么要用偏向锁延迟,JVM启动时,某个类有一个线程访问,那么它为偏向锁,当第二个线程来访问的时候,会锁消除升级为轻量级锁,这个过程
是相当发杂且耗费资源的,JVM可以确定虚拟机启动时大部分为轻量级锁,所以JDK做了优化,等JVM启动完成后,在加载偏向锁,不用进行锁消除 升级操作
轻量级锁和偏向锁的性能对比,差距十分之大,偏向锁的性能高,轻量级锁性能低。 十倍左右的差距
调用wait()方法后立即升级为重量级锁
释放锁跟修改对象头mark word中锁状态的值不是一个东西,释放锁是释放锁,改对象头信息是改对象头信息,不一样。同步代码块执行完都会释放锁,并不一定会
改变对象状态的标志位
偏向锁在退出同步代码块后锁依然保持偏向信息,当一个轻量/重量级锁在退出代码块后会将锁状态会消除还原为一个无锁状态,因为锁的膨胀过程。
为什么在没有同步之前对象也是偏向锁?
轻量级锁是什么时候发生的:线程交替执行无竞争
同步{
1.单线程
2.多个线程交替执行 一般为轻量级锁,当发生互斥的时候,第二个线程会自旋,等待一个时间(有资料说为一个线程上下文切换的时间),
如果在自选时间内获取到了锁,依然为轻量级锁,获取不到升级为重量级锁。自旋锁
3.多个线程互斥执行 一般为重量级锁
}