1. 程式人生 > >java物件頭資訊和三種鎖的效能對比

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