java物件頭資訊和三種鎖的效能對比
java頭的資訊分析
首先為什麼我要去研究java的物件頭呢? 這裡擷取一張hotspot的原始碼當中的註釋
這張圖換成可讀的表格如下
|--------------------------------------------------------------------------------------------------------------| | Object Header (128 bits) | |--------------------------------------------------------------------------------------------------------------| | Mark Word (64 bits) | Klass Word (64 bits) | |--------------------------------------------------------------------------------------------------------------| | 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 | 重量鎖 |----------------------------------------------------------------------|--------|------------------------------| | | lock:2 | OOP to metadata object | GC |--------------------------------------------------------------------------------------------------------------|
意思是java的物件頭在物件的不同狀態下會有不同的表現形式,主要有三種狀態,無鎖狀態、加鎖狀態、gc標記狀態。
那麼我可以理解java當中的取鎖其實可以理解是給物件上鎖,也就是改變物件頭的狀態,如果上鎖成功則進入同步程式碼塊。
但是java當中的鎖有分為很多種,從上圖可以看出大體分為偏向鎖、輕量鎖、重量鎖三種鎖狀態。
這三種鎖的效率 完全不同、關於效率的分析會在下文分析,我們只有合理的設計程式碼,才能合理的利用鎖、那麼這三種鎖的原理是什麼? 所以我們需要先研究這個物件頭。
java物件的佈局以及物件頭的佈局
使用JOL來分析java的物件佈局,新增依賴
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.8</version> </dependency>
測試類
public class B { }
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()); } }
看下結果
分析結果1:整個物件一共16B,其中物件頭(Object header)12B,還有4B是對齊的位元組(因為在64位虛擬機器上物件的大小必 須是8的倍數),
由於這個物件裡面沒有任何欄位,故而物件的例項資料為0B?
兩個問題
1、什麼叫做物件的例項資料呢?
2、那麼物件頭裡面的12B到底存的是什麼呢?
首先要明白什麼物件的例項資料很簡單,我們可以在B當中新增一個boolean的欄位,大家都知道boolean欄位佔 1B,然後再看結果
整個物件的大小還是沒有改變一共16B,其中物件頭(Object header)12B,boolean欄位flag(物件的例項資料)佔 1B、剩下的3B就是對齊位元組。
由此我們可以認為一個物件的佈局大體分為三個部分分別是:物件頭(Object header)、 物件的例項資料和位元組對齊
接下來討論第二個問題,物件頭為什麼是12B?這個12B當中分別儲存的是什麼呢?(不同位數的VM物件頭的長度不一 樣,這裡指的是64bit的vm)
首先引用openjdk文件當中對物件頭的解釋
上述引用中提到一個java物件頭包含了2個word,並且好包含了堆物件的佈局、型別、GC狀態、同步狀態和標識哈 希碼,具體怎麼包含的呢?又是哪兩個word呢?
mark word為第一個word根據文件可以知他裡面包含了鎖的資訊,hashcode,gc資訊等等,第二個word是什麼 呢?
klass word為物件頭的第二個word主要指向物件的元資料。
假設我們理解一個物件頭主要上圖兩部分組成(陣列物件除外,陣列物件的物件頭還包含一個數組長度),
那麼 一個java的物件頭多大呢?我們從JVM的原始碼註釋中得知到一個mark word一個是64bit,那麼klass的長度是多少呢?
所以我們需要想辦法來獲得java物件頭的詳細資訊,驗證一下他的大小,驗證一下里麵包含的資訊是否正確。
根據上述利用JOL列印的物件頭資訊可以知道一個物件頭是12B,其中8B是mark word 那麼剩下的4B就是klass word了,和鎖相關的就是mark word了,
那麼接下來重點分析mark word裡面資訊 在無鎖的情況下markword當中的前56bit存的是物件的hashcode,那麼來驗證一下
先上程式碼:手動計算HashCode
public class HashUtil { public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException { // 手動計算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中的每一個Byte進行計算 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"); //沒有計算HASHCODE之前的物件頭 out.println(ClassLayout.parseInstance(b).toPrintable()); //JVM 計算的hashcode out.println("jvm------------0x"+Integer.toHexString(b.hashCode())); HashUtil.countHash(b); //當計算完hashcode之後,我們可以檢視物件頭的資訊變化 out.println("after hash"); out.println(ClassLayout.parseInstance(b).toPrintable()); } }
分析結果3:
1-----上面沒有進行hashcode之前的物件頭資訊,可以看到的56bit沒有值,列印完hashcode之後就有值了,為什 麼是1-7B,不是0-6B呢?因為是小端儲存。
其中兩行是我們通過hashcode方法列印的結果,第一行是我根據1-7B的資訊計算出來的 hashcode,所以可以確定java物件頭當中的mark work裡面的後七個位元組儲存的是hashcode資訊,
那麼第一個位元組當中的八位分別存的 就是分帶年齡、偏向鎖資訊,和物件狀態,這個8bit分別表示的資訊如下圖(其實上圖也有資訊),這個圖會隨著物件狀態改變而改變, 下圖是無鎖狀態下
關於物件狀態一共分為五種狀態,分別是無鎖、偏向鎖、輕量鎖、重量鎖、GC標記,
那麼2bit,如何能表示五種狀 態(2bit最多隻能表示4中狀態分別是:00,01,10,11),
jvm做的比較好的是把偏向鎖和無鎖狀態表示為同一個狀態,然 後根據圖中偏向鎖的標識再去標識是無鎖還是偏向鎖狀態。
什麼意思呢?寫個程式碼分析一下,在寫程式碼之前我們先記得 無鎖狀態下的資訊00000001,然後寫一個偏向鎖的例子看看結果
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());
}
上面這個程式只有一個執行緒去呼叫sync方法,故而講道理應該是偏向鎖,但是此時卻是輕量級鎖
而且你會發現最後輸出的結果(第一個位元組)依 然是00000001和無鎖的時候一模一樣,其實這是因為虛擬機器在啟動的時候對於偏向鎖有延遲,
比如把上述程式碼當中加上 睡眠5秒的程式碼,結果就會不一樣了,
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()); }
結果變成00000101.當然為了方便測試我們也可以直接通過JVM的引數來禁用延遲
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
結果是和睡眠5秒一樣的.
想想為什麼偏向鎖會延遲?因為啟動程式的時候,jvm會有很多操作,包括gc等等,jvm剛執行時存在大量的同步方法,很多都不是偏向鎖,
而偏向鎖升級為輕/重量級鎖的很費時間和資源,因此jvm會延遲4秒左右再開啟偏向鎖.
那麼為什麼同步之前就是偏向鎖呢?我猜想是jvm的原因,目前還不清楚.
需要注意的after lock,退出同步後依然保持了偏向資訊
然後看下輕量級鎖的物件頭
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()); }
看結果:
關於重量鎖首先看物件頭
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());//重量鎖 } }
看結果
由上述實驗可總結下圖:
效能對比偏向鎖和輕量級鎖:
public class A { int i=0; public synchronized void parse(){ i++; } //JOLExample6.countDownLatch.countDown(); }
執行1000000000L次++操作
public class JOLExample4 { public static void main(String[] args) throws Exception { A a = new A(); long start = System.currentTimeMillis(); //呼叫同步方法1000000000L 來計算1000000000L的++,對比偏向鎖和輕量級鎖的效能 //如果不出意外,結果灰常明顯 for(int i=0;i<1000000000L;i++){ a.parse(); } long end = System.currentTimeMillis(); System.out.println(String.format("%sms", end - start)); } }
此時根據上面的測試可知是輕量級鎖,看下結果
大概16秒
然後我們讓偏向鎖啟動無延時,在啟動一次
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
再看下結果
只需要2秒,速度提升了很多
再看下重量級鎖的時間
static CountDownLatch countDownLatch = new CountDownLatch(1000000000); public static void main(String[] args) throws Exception { final A a = new A(); long start = System.currentTimeMillis(); //呼叫同步方法1000000000L 來計算1000000000L的++,對比偏向鎖和輕量級鎖的效能 //如果不出意外,結果灰常明顯 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)); }
看下結果,大概31秒,
可以看出三種鎖的消耗是差距很大的,這也是1.5以後synchronized優化的意義
需要注意的是如果物件已經計算了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()); }
看下結果
&n