盤一盤 synchronized (一)—— 從打印Java對象頭說起


Java對象頭的組成

Java對象的對象頭由 mark word 和  klass pointer 兩部分組成,
mark word存儲了同步狀態、標識、hashcode、GC狀態等等。
klass pointer存儲對象的類型指針,該指針指向它的類元數據
值得注意的是,如果應用的對象過多,使用64位的指針將浪費大量內存。64位的JVM比32位的JVM多耗費50%的內存。
我們現在使用的64位 JVM會默認使用選項 +UseCompressedOops 開啟指針壓縮,將指針壓縮至32位。
 
 
以64位操作系統為例,對象頭存儲內容圖例。
|--------------------------------------------------------------------------------------------------------------|
|                                              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
|--------------------------------------------------------------------------------------------------------------|
簡單介紹一下各部分的含義
lock:  鎖狀態標記位,該標記的值不同,整個mark word表示的含義不同。
biased_lock:偏向鎖標記,為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖。
age:Java GC標記位對象年齡。
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 Exception {
Thread.sleep(5000);
A a = new A();

Thread thread1= new Thread(){
@Override
public void run() {
synchronized (a){
System.out.println("thread1 locking");
out.println(ClassLayout.parseInstance(a).toPrintable()); //偏向鎖
}
}
};
thread1.start();
thread1.join();
Thread.sleep(10000);

synchronized (a){
out.println("main locking");
out.println(ClassLayout.parseInstance(a).toPrintable());//輕量鎖
}
}

thread1中依舊輸出偏向鎖,主線程獲取對象A時,thread1雖然已經退出同步代碼塊,但主線程和thread1仍然為鎖的交替競爭關系。故此時主線程輸出結果為輕量級鎖。

 

重量級鎖

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,此時輸出結果為重量級鎖

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM