Java對象頭的組成
java對象頭分爲兩部分, mark word 和 klass pointer。
mark word 包括: 對象hashcode,,分代年齡4bit,偏向鎖1bit,鎖 類型2bit。
klass pointer存儲對象的類型指針,該指針指向它的類元數據。
64位 JVM會默認使用選項 +UseCompressedOops 開啓指針壓縮,將指針壓縮至32位。
以64位操作系統爲例,對象頭存儲內容圖例。
其中,lock: 鎖狀態標記位,該標記的值不同,整個mark word表示的含義不同。
biased_lock:偏向鎖標記,爲1時表示對象啓用偏向鎖,爲0時表示對象沒有偏向鎖。
age:Java GC標記位對象年齡。爲什麼默認是16歲回收,因爲只有4bit,4bit 1111最大是15。
identity_hashcode:對象標識Hash碼,採用延遲加載技術。當對象使用HashCode()計算後,並會將結果寫到該對象頭中。當對象被鎖定時,該值會移動到線程Monitor中。
thread:持有偏向鎖的線程ID和其他信息。這個線程ID並不是JVM分配的線程ID號,和Java Thread中的ID是兩個概念。
epoch:偏向時間戳。
ptr_to_lock_record:指向棧中鎖記錄的指針。
ptr_to_heavyweight_monitor:指向線程Monitor的指針。
使用JOL工具類,打印對象頭#
使用maven的方式,添加jol依賴
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.8</version> </dependency>
創建一個對象A
public class A { boolean flag = false; }
使用jol工具類輸出A對象的對象頭
public static void main(String[] args){ A a = new A(); System.out.println(ClassLayout.parseInstance(a).toPrintable()); }
看看輸出結果
輸出的第一行內容和鎖狀態內容對應
unused:1 | age:4 | biased_lock:1 | lock:2
0 0000 0 01 代表A對象正處於無鎖狀態
第三行中表示的是被指針壓縮爲32位的klass pointer
第四行則是我們創建的A對象屬性信息 1字節的boolean值
第五行則代表了對象的對齊字段 爲了湊齊64位的對象,對齊字段佔用了3個字節,24bit
偏向鎖#
public static void main(String[] args) throws InterruptedException { Thread.sleep(5000); A a = new A(); System.out.println(ClassLayout.parseInstance(a).toPrintable()); }
轉存失敗重新上傳取消轉存失敗重新上傳取消轉存失敗重新上傳取消轉存失敗重新上傳取消
轉存失敗重新上傳取消轉存失敗重新上傳取消轉存失敗重新上傳取消轉存失敗重新上傳取消輸出結果
剛開始使用這段代碼我是震驚的,爲什麼睡眠了5s中就把活生生的A對象由無鎖狀態改變成爲偏向鎖了呢?別急,容我慢慢道來!
JVM啓動時會進行一系列的複雜活動,比如裝載配置,系統類初始化等等。在這個過程中會使用大量synchronized關鍵字對對象加鎖,且這些鎖大多數都不是偏向鎖。爲了減少初始化時間,JVM默認延時加載偏向鎖。這個延時的時間大概爲4s左右,具體時間因機器而異。當然我們也可以設置JVM參數 -XX:BiasedLockingStartupDelay=0 來取消延時加載偏向鎖。
可能你又要問了,我這也沒使用synchronized關鍵字呀,那不也應該是無鎖麼?怎麼會是偏向鎖呢?
仔細看一下偏向鎖的組成,對照輸出結果紅色劃線位置,你會發現佔用 thread 和 epoch 的 位置的均爲0,說明當前偏向鎖並沒有偏向任何線程。此時這個偏向鎖正處於可偏向狀態,準備好進行偏向了!你也可以理解爲此時的偏向鎖是一個特殊狀態的無鎖。
大家可以看下面這張圖理解一下對象頭的狀態的創建過程
再來看看這段代碼,使用了synchronized關鍵字
public static void main(String[] args) throws InterruptedException { Thread.sleep(5000); A a = new A(); synchronized (a){ System.out.println(ClassLayout.parseInstance(a).toPrintable()); } }
此時對象a,對象頭內容有了明顯的變化,當前偏向鎖偏向主線程。
輕量級鎖#
public static void main(String[] args) throws InterruptedException { Thread.sleep(5000); A a = new A(); Thread thread1 = new Thread(){ @Override public void run() { synchronized (a){ System.out.println("thread1 locking"); System.out.println(ClassLayout.parseInstance(a).toPrintable()); } try { //thread1退出同步代碼塊,且沒有死亡 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread thread2 = new Thread(){ @Override public void run() { synchronized (a){ System.out.println("thread2 locking"); System.out.println(ClassLayout.parseInstance(a).toPrintable()); } } }; thread1.start(); //讓thread1執行完同步代碼塊中方法。 Thread.sleep(3000); thread2.start(); }
thread1中依舊輸出偏向鎖,thread2獲取對象A時,thread1已經退出同步代碼塊,故此時thread2輸出結果爲輕量級鎖。
重量級鎖#
public static void main(String[] args) throws InterruptedException { Thread.sleep(5000); A a = new A(); Thread thread1 = new Thread(){ @Override public void run() { synchronized (a){ System.out.println("thread1 locking"); System.out.println(ClassLayout.parseInstance(a).toPrintable()); try { //讓線程晚點兒死亡,造成鎖的競爭 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread thread2 = new Thread(){ @Override public void run() { synchronized (a){ System.out.println("thread2 locking"); System.out.println(ClassLayout.parseInstance(a).toPrintable()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread1.start(); thread2.start(); }
thread1 和 thread2 同時競爭對象a,此時輸出結果爲重量級鎖