UNSAFE和Java 內存佈局

最近在翻ReentrantLock源碼的時候,看到AQS(AbstractQueuedSynchronizer.java)裏面有一段代碼

 

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

這就是經典的CAS的算法,這裏包含兩個陌生的東西,unsafe,stateOffset。

 

private static final Unsafe unsafe = Unsafe.getUnsafe();

stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));

又發現stateOffset是跟AQS裏面的state字段相關

 

private volatile int state;

然後我們又發現state是volatitle類型的,當然這是實現LOCK必備的。

思考

這個stateOffset是什麼,值是多少,由stateOffset能得到什麼?由CAS的算法我們知道需要跟原值進行對比,所以大膽推測通過stateOffset可以得到state字段的值。

另外還有一個東西很讓人好奇,UNSAFE是什麼,能做什麼?

粗略認識

帶着這兩個問題,查了不少資料,這裏我希望儘量能用白話的方式說明一下。
UNSAFE,顧名思義是不安全的,他的不安全是因爲他的權限很大,可以調用操作系統底層直接操作內存空間,所以一般不允許使用。
可參考:java對象的內存佈局(二):利用sun.misc.Unsafe獲取類字段的偏移地址和讀取字段的值
我們注意到上面有一個方法

  • stateOffset=unsafe.objectFieldOffset(field) 從方法名上可以這樣理解:獲取object對象的屬性Field的偏移量。

要理解這個偏移量,需要先了解java的內存模型

Java內存模型

 

image.png


此文章值得認真閱讀幾遍: java對象在內存中的結構(HotSpot虛擬機)

 

Java對象在內存中存儲的佈局可以分爲三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding),簡單的理解:

  • 對象頭,對象是什麼?
  • 實例數據,對象裏有什麼?
  • 對齊填充,不關鍵,目的是補齊位數達到8的倍數。
    參考:對象的內存佈局

    image.png

舉個簡單的例子,如下類:

 

class VO {
    public int a = 0;
    public int b = 0;
}

VO vo=new VO();的時候,Java內存中就開闢了一塊地址,包含一個固定長度的對象頭(假設是16字節,不同位數機器/對象頭是否壓縮都會影響對象頭長度)+實例數據(4字節的a+4字節的b)+padding。

這裏直接說結論,我們上面說的偏移量就是在這裏體現,如上面a屬性的偏移量就是16,b屬性的偏移量就是20。

在unsafe類裏面,我們發現一個方法unsafe.getInt(object, offset);
通過unsafe.getInt(vo, 16) 就可以得到vo.a的值。是不是聯想到反射了?其實java的反射底層就是用的UNSAFE(具體如何實現,預留到以後研究)。

進一步思考

如何知道一個類裏面每個屬性的偏移量?只根據偏移量,java怎麼知道讀取到哪裏爲止是這個屬性的值?

查看屬性偏移量,推薦一個工具類jol:http://openjdk.java.net/projects/code-tools/jol/
用jol可以很方便的查看java的內存佈局情況,結合一下代碼講解

 

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>

 

public class VO {
    public int a = 0;
    public long b = 0;
    public String c= "123";
    public Object d= null;
    public int e = 100;
    public static int f= 0;
    public static String g= "";
    public Object h= null;
    public boolean i;
}

 

    public static void main(String[] args) throws Exception {
        System.out.println(VM.current().details());
        System.out.println(ClassLayout.parseClass(VO.class).toPrintable());
        System.out.println("=================");
        Unsafe unsafe = getUnsafeInstance();
        VO vo = new VO();
        vo.a=2;
        vo.b=3;
        vo.d=new HashMap<>();
        long aoffset = unsafe.objectFieldOffset(VO.class.getDeclaredField("a"));
        System.out.println("aoffset="+aoffset);
        // 獲取a的值
        int va = unsafe.getInt(vo, aoffset);
        System.out.println("va="+va);
    }

    public static Unsafe getUnsafeInstance() throws Exception {
        // 通過反射獲取rt.jar下的Unsafe類
        Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeInstance.setAccessible(true);
        // return (Unsafe) theUnsafeInstance.get(null);是等價的
        return (Unsafe) theUnsafeInstance.get(Unsafe.class);
    }

在我本地機器測試結果如下:

 

# Running 64-bit HotSpot VM.
# Using compressed oop with 0-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

com.ha.net.nsp.product.VO object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0    12                    (object header)                           N/A
     12     4                int VO.a                                      N/A
     16     8               long VO.b                                      N/A
     24     4                int VO.e                                      N/A
     28     1            boolean VO.i                                      N/A
     29     3                    (alignment/padding gap)                  
     32     4   java.lang.String VO.c                                      N/A
     36     4   java.lang.Object VO.d                                      N/A
     40     4   java.lang.Object VO.h                                      N/A
     44     4                    (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

=================
aoffset=12
va=2

在結果中,我們發現:

  • 1、我本地的虛擬機環境是64位並且開啓了compressed壓縮,對象都是8字節對齊
  • 2、VO類的內存佈局包含12字節的對象頭,4字節的int數據,8字節的long數據,其他String和Object是4字節,最後還有4字節的對齊。
  • 3、VO類屬性的內存佈局跟屬性聲明的順序不一致。
  • 4、VO類的static屬性不在VO的內存佈局中,因爲他是屬於class類。
  • 5、通過VO類就可以確定一個對象佔用的字節數,這個佔用空間在編譯階段就已經確定(注:此佔用空間並不是對象的真實佔用空間,)。
  • 6、如上,通過偏移量12就可以讀取到此處存放的值是2。

引申出新的問題:
1、這裏的對象頭爲什麼是12字節?對象頭裏都具體包含什麼?
答:正常情況下,對象頭在32位系統內佔用一個機器碼也就是8個字節,64位系統也是佔用一個機器碼16個字節。但是在我本地環境是開啓了reference(指針)壓縮,所以只有12個字節。
2、這裏的String和Object爲什麼都是4字節?
答:因爲String或者Object類型,在內存佈局中,都是reference類型,所以他的大小跟是否啓動壓縮有關。未啓動壓縮的時候,32位機器的reference類型是4個字節,64位是8個字節,但是如果啓動壓縮後,64位機器的reference類型就變成4字節。
3、Java怎麼知道應該從偏移量12讀取到偏移量16呢,而不是讀取到偏移量18或者20?
答:這裏我猜測,虛擬機在編譯階段,就已經保留了一個VO類的偏移量數組,那12後的偏移量就是16,所以Java知道讀到16爲止。

更多內存佈局問題請參考:
java對象的內存佈局(一):計算java對象佔用的內存空間以及java object layout工具的使用
Java對象內存結構
JVM內存堆佈局圖解分析

對象頭包含什麼內容

java中的對象頭的解析

image.png

 

  • 1、對象頭有幾位是鎖標誌位
    可以參考如下文章,對象頭跟鎖有很重要的關聯,並且文章中提到另外一個概念:Monitor,預留到以後研究
    死磕Java併發:深入分析synchronized的實現原理

  • 2、對象頭有幾位代表分代年齡,與回收算法有關
    CMS標記-清除回收算法,標記階段的大概過程是從棧中查找所有的reference類型,遞歸可達的所有堆內對象的對象頭都標記爲數據可達,清除階段是對堆內存從頭到尾進行線性遍歷,如果發現有對象沒有被標識爲可到達對象,那麼就將此對象佔用的內存回收,並且將原來標記爲可到達對象的標識清除。
    在 gc回收的時候,會更新還存活的對象的對象頭的分代年齡,同時如果這些對象還有發生位置移動(碎片清理),那麼還要重新計算對象頭的hash值,以及棧中相應的reference引用的值。

說到回收算法,再參考下這篇也更能理解對象的創建和回收:
垃圾回收機制中,引用計數法是如何維護所有對象引用的?

UNSAFE與線程的關係

unsafe中有一個park方法,與線程掛起有關,預留到以後研究

參考資料

一個Java對象到底佔多大內存?

JVM內存模型及String對象內存分配



轉自:https://www.jianshu.com/p/cb5e09facfee

發佈了21 篇原創文章 · 獲贊 25 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章