Java 對(duì)象頭信息分析和三種鎖的性能對(duì)比
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進(jìn)!你不來,我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!
編輯:業(yè)余草
推薦:https://www.xttblog.com/?p=5277
Java 頭的信息分析
首先為什么我要去研究 java 的對(duì)象頭呢?這里截取一張 hotspot 的源碼當(dāng)中的注釋。

這張圖換成可讀的表格如下:

意思是 java 的對(duì)象頭在對(duì)象的不同狀態(tài)下會(huì)有不同的表現(xiàn)形式,主要有三種狀態(tài),無鎖狀態(tài)、加鎖狀態(tài)、gc 標(biāo)記狀態(tài)。
那么我可以理解 java 當(dāng)中的取鎖其實(shí)可以理解是給對(duì)象上鎖,也就是改變對(duì)象頭的狀態(tài),如果上鎖成功則進(jìn)入同步代碼塊。
但是 java 當(dāng)中的鎖有分為很多種,從上圖可以看出大體分為偏向鎖、輕量鎖、重量鎖三種鎖狀態(tài)。
這三種鎖的效率 完全不同、關(guān)于效率的分析會(huì)在下文分析,我們只有合理的設(shè)計(jì)代碼,才能合理的利用鎖、那么這三種鎖的原理是什么? 所以我們需要先研究這個(gè)對(duì)象頭。
java對(duì)象的布局以及對(duì)象頭的布局
使用 JOL 來分析 java 的對(duì)象布局,添加依賴。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
測(cè)試類:
public class JOLExample1 {
static B b = new B();
public static void main(String [] args) {
//jvm的信息
out.println(VM.current().details());
out.println(ClassLayout.parseInstance(b).toPrintable());
}
}
看下結(jié)果:

分析結(jié)果 1:整個(gè)對(duì)象一共 16B,其中對(duì)象頭(Object header)12B,還有 4B 是對(duì)齊的字節(jié)(因?yàn)樵?64 位虛擬機(jī)上對(duì)象的大小必 須是 8 的倍數(shù))。
由于這個(gè)對(duì)象里面沒有任何字段,故而對(duì)象的實(shí)例數(shù)據(jù)為 0B?
兩個(gè)問題:
1、什么叫做對(duì)象的實(shí)例數(shù)據(jù)呢?
2、那么對(duì)象頭里面的 12B 到底存的是什么呢?
首先要明白什么對(duì)象的實(shí)例數(shù)據(jù)很簡(jiǎn)單,我們可以在 B 當(dāng)中添加一個(gè) boolean 的字段,大家都知道 boolean 字段占 1B,然后再看結(jié)果。

整個(gè)對(duì)象的大小還是沒有改變一共 16B,其中對(duì)象頭(Object header)12B, boolean 字段 flag(對(duì)象的實(shí)例數(shù)據(jù))占 1B、剩下的 3B 就是對(duì)齊字節(jié)。
由此我們可以認(rèn)為一個(gè)對(duì)象的布局大體分為三個(gè)部分分別是:對(duì)象頭(Object header)、 對(duì)象的實(shí)例數(shù)據(jù)和字節(jié)對(duì)齊。
接下來討論第二個(gè)問題,對(duì)象頭為什么是 12B?這個(gè) 12B 當(dāng)中分別存儲(chǔ)的是什么呢?(不同位數(shù)的 VM 對(duì)象頭的長度不一 樣,這里指的是 64bit 的 vm)。
首先引用 openjdk 文檔當(dāng)中對(duì)對(duì)象頭的解釋:

上述引用中提到一個(gè) java 對(duì)象頭包含了 2 個(gè) word,并且好包含了堆對(duì)象的布局、類型、GC 狀態(tài)、同步狀態(tài)和標(biāo)識(shí)哈希碼,具體怎么包含的呢?又是哪兩個(gè) word呢?

mark word 為第一個(gè) word 根據(jù)文檔可以知他里面包含了鎖的信息,hashcode,gc 信息等等,第二個(gè) word 是什么 呢?

klass word 為對(duì)象頭的第二個(gè) word 主要指向?qū)ο蟮脑獢?shù)據(jù)。

假設(shè)我們理解一個(gè)對(duì)象頭主要上圖兩部分組成(數(shù)組對(duì)象除外,數(shù)組對(duì)象的對(duì)象頭還包含一個(gè)數(shù)組長度)。
那么 一個(gè) java 的對(duì)象頭多大呢?我們從 JVM 的源碼注釋中得知到一個(gè) mark word 一個(gè)是 64bit,那么 klass 的長度是多少呢?
所以我們需要想辦法來獲得 java 對(duì)象頭的詳細(xì)信息,驗(yàn)證一下他的大小,驗(yàn)證一下里面包含的信息是否正確。
根據(jù)上述利用 JOL 打印的對(duì)象頭信息可以知道一個(gè)對(duì)象頭是 12B,其中 8B 是 mark word 那么剩下的 4B 就是 klass word 了,和鎖相關(guān)的就是 mark word 了。
那么接下來重點(diǎn)分析 mark word 里面信息 在無鎖的情況下 markword 當(dāng)中的前 56bit 存的是對(duì)象的 hashcode,那么來驗(yàn)證一下
先上代碼:手動(dòng)計(jì)算 HashCode。
public class HashUtil {
public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException {
// 手動(dòng)計(jì)算HashCode
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
long hashCode = 0;
for (long index = 7; index > 0; index--) {
// 取Mark Word中的每一個(gè)Byte進(jìn)行計(jì)算
hashCode |= (unsafe.getByte(object, index) & 0xFF) << ((index - 1) * 8);
}
String code = Long.toHexString(hashCode);
System.out.println("util-----------0x"+code);
}
}
public class JOLExample2 {
public static void main(String[] args) throws Exception {
B b = new B();
out.println("befor hash");
//沒有計(jì)算HASHCODE之前的對(duì)象頭
out.println(ClassLayout.parseInstance(b).toPrintable());
//JVM 計(jì)算的hashcode
out.println("jvm------------0x"+Integer.toHexString(b.hashCode()));
HashUtil.countHash(b);
//當(dāng)計(jì)算完hashcode之后,我們可以查看對(duì)象頭的信息變化
out.println("after hash");
out.println(ClassLayout.parseInstance(b).toPrintable());
}
}

分析結(jié)果 3:
上面沒有進(jìn)行 hashcode 之前的對(duì)象頭信息,可以看到的 56bit 沒有值,打印完 hashcode 之后就有值了,為什么是 1-7B,不是 0-6B 呢?因?yàn)槭?strong style="color: rgb(53, 148, 247);">「小端存儲(chǔ)」。
其中兩行是我們通過 hashcode 方法打印的結(jié)果,第一行是我根據(jù) 1-7B 的信息計(jì)算出來的 hashcode,所以可以確定 java 對(duì)象頭當(dāng)中的 mark work 里面的后七個(gè)字節(jié)存儲(chǔ)的是 hashcode 信息。
那么第一個(gè)字節(jié)當(dāng)中的八位分別存的就是分帶年齡、偏向鎖信息,和對(duì)象狀態(tài),這個(gè) 8bit 分別表示的信息如下圖(其實(shí)上圖也有信息),這個(gè)圖會(huì)隨著對(duì)象狀態(tài)改變而改變,下圖是無鎖狀態(tài)下。

關(guān)于對(duì)象狀態(tài)一共分為五種狀態(tài),分別是無鎖、偏向鎖、輕量鎖、重量鎖、GC 標(biāo)記。
那么 2bit,如何能表示五種狀 態(tài)(2bit 最多只能表示 4 中狀態(tài)分別是:00,01,10,11)。
jvm 做的比較好的是把偏向鎖和無鎖狀態(tài)表示為同一個(gè)狀態(tài),然 后根據(jù)圖中偏向鎖的標(biāo)識(shí)再去標(biāo)識(shí)是無鎖還是偏向鎖狀態(tài)。
什么意思呢?寫個(gè)代碼分析一下,「在寫代碼之前我們先記得 無鎖狀態(tài)下的信息00000001」,然后寫一個(gè)偏向鎖的例子看看結(jié)果。
public static void main(String[] args) throws Exception {
//-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
B b = new B();
out.println("befor lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
synchronized (b){
out.println("lock ing");
out.println(ClassLayout.parseInstance(b).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
}

上面這個(gè)程序只有一個(gè)線程去調(diào)用 sync 方法,故而講道理應(yīng)該是偏向鎖,但是此時(shí)卻是輕量級(jí)鎖。
而且你會(huì)發(fā)現(xiàn)最后輸出的結(jié)果(第一個(gè)字節(jié))依 然是 00000001 和無鎖的時(shí)候一模一樣,其實(shí)這是因?yàn)樘摂M機(jī)在啟動(dòng)的時(shí)候?qū)τ谄蜴i有延遲。
比如把上述代碼當(dāng)中加上睡眠 5 秒的代碼,結(jié)果就會(huì)不一樣了。
public static void main(String[] args) throws Exception {
//-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
Thread.sleep(5000);
B b = new B();
out.println("befor lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
synchronized (b){
out.println("lock ing");
out.println(ClassLayout.parseInstance(b).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
}

結(jié)果變成 00000101。當(dāng)然為了方便測(cè)試我們也可以直接通過 JVM 的參數(shù)來禁用延遲。
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

結(jié)果是和睡眠 5 秒一樣的。
想想為什么偏向鎖會(huì)延遲?因?yàn)閱?dòng)程序的時(shí)候,jvm 會(huì)有很多操作,包括 gc 等等,jvm 剛運(yùn)行時(shí)存在大量的同步方法,很多都不是偏向鎖。
「而偏向鎖升級(jí)為輕/重量級(jí)鎖的很費(fèi)時(shí)間和資源,因此 jvm 會(huì)延遲 4 秒左右再開啟偏向鎖?!?/strong>
「那么為什么同步之前就是偏向鎖呢?我猜想是 jvm 的原因,目前還不清楚?!?/strong>
需要注意的 after lock,退出同步后依然保持了偏向信息。
然后看下輕量級(jí)鎖的對(duì)象頭。
static A a;
public static void main(String[] args) throws Exception {
a = new A();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
看結(jié)果:

關(guān)于重量鎖首先看對(duì)象頭。
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());//無鎖
Thread t1= new Thread(){
public void run() {
synchronized (a){
try {
Thread.sleep(5000);
System.out.println("t1 release");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread.sleep(1000);
out.println("t1 lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());//輕量鎖
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());//重量鎖
System.gc();
out.println("after gc()");
out.println(ClassLayout.parseInstance(a).toPrintable());//無鎖---gc
}
public static void sync() throws InterruptedException {
synchronized (a){
System.out.println("t1 main lock");
out.println(ClassLayout.parseInstance(a).toPrintable());//重量鎖
}
}
看結(jié)果。


「由上述實(shí)驗(yàn)可總結(jié)下圖:」

性能對(duì)比偏向鎖和輕量級(jí)鎖:
public class A {
int i=0;
public synchronized void parse(){
i++;
}
//JOLExample6.countDownLatch.countDown();
}
執(zhí)行 1000000000L 次 ++ 操作。
public class JOLExample4 {
public static void main(String[] args) throws Exception {
A a = new A();
long start = System.currentTimeMillis();
//調(diào)用同步方法1000000000L 來計(jì)算1000000000L的++,對(duì)比偏向鎖和輕量級(jí)鎖的性能
//如果不出意外,結(jié)果灰常明顯
for(int i=0;i<1000000000L;i++){
a.parse();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}
}
此時(shí)根據(jù)上面的測(cè)試可知是輕量級(jí)鎖,看下結(jié)果。

大概 16 秒。
然后我們讓偏向鎖啟動(dòng)無延時(shí),在啟動(dòng)一次。
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
再看下結(jié)果。

只需要 2 秒,速度提升了很多。
再看下重量級(jí)鎖的時(shí)間。
static CountDownLatch countDownLatch = new CountDownLatch(1000000000);
public static void main(String[] args) throws Exception {
final A a = new A();
long start = System.currentTimeMillis();
//調(diào)用同步方法1000000000L 來計(jì)算1000000000L的++,對(duì)比偏向鎖和輕量級(jí)鎖的性能
//如果不出意外,結(jié)果灰常明顯
for(int i=0;i<2;i++){
new Thread(){
@Override
public void run() {
while (countDownLatch.getCount() > 0) {
a.parse();
}
}
}.start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}
看下結(jié)果,大概 31 秒。

可以看出三種鎖的消耗是差距很大的,這也是 1.5 以后 synchronized 優(yōu)化的意義。
需要注意的是如果對(duì)象已經(jīng)計(jì)算了 hashcode 就不能偏向了
static A a;
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
a= new A();
a.hashCode();
out.println("befor lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
看下結(jié)果。

