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());
    }

看下結果