99.9%的Java程序員都說不清的問題:JVM中的對象內存佈局?

做者:李瑞傑算法

目前就任於阿里巴巴,資深 JVM 研究人員數組


在 Java 程序中,咱們擁有多種新建對象的方式。除了最爲常見的 new 語句以外,咱們還能夠經過反射機制、Object.clone 方法、反序列化以及 Unsafe.allocateInstance 方法來新建對象。
緩存

其中,Object.clone 方法和反序列化經過直接複製已有的數據,來初始化新建對象的實例字段。架構

Unsafe.allocateInstance 方法則沒有初始化實例字段,而 new 語句和反射機制,則是經過調用構造器來初始化實例字段。
工具

咱們先來考察new語句,準備一個類,以下圖所示佈局



讓咱們編譯他的字節碼:學習


能夠看到,new語句編譯而成的字節碼將包含用來請求內存的 new 指令,以及用來調用構造器的 invokespecial 指令。
this

本文不是專門介紹invoke系列指令的,我會在後面的文章中介紹invoke系列指令。線程

不過在這裏我多說一嘴,字節碼中的invokespecial指令一般用於調用私有實例方法、構造器,以及使用super關鍵字調用父類的實例方法或構造器,和所實現接口的默認方法。指針

提到構造器,就不得不提到 Java 對構造器的諸多約束。首先,若是一個類沒有定義任何構造器的話, Java 編譯器會自動添加一個無參數的構造器。

咱們剛纔的TestNew類,他的字節碼編譯出來後,有下面的片斷。


在JAVA源碼中,咱們沒有定義構造器,可是生成出來的字節碼,已經自動幫咱們添加了一個無參數的構造器。他使用的invokespecial方法最終調用的是其父類Object類的構造器方法。

我將講述JVM的構造器調用原則,那就是,若是子類的構造器須要調用父類的構造器。若是父類存在無參數構造器的話,該調用能夠是隱式的。也就是說, Java 編譯器會自動添加對父類構造器的調用。

可是,若是父類沒有無參數構造器,那麼子類的構造器則須要顯式地調用父類帶參數的構造器。

顯式調用有兩種,一是直接使用「super」關鍵字調用父類構造器,二是使用「this」關鍵字調用同一個類中的其餘構造器。

不管是直接的顯式調用,仍是間接的顯式調用,都須要做爲構造器的第一條語句,以便優先初始化繼承而來的父類字段。

能夠不優先初始化繼承來的父類字段嗎?能夠,若是你能使用字節碼注入工具的話。

當咱們調用一個構造器時,它將優先調用父類的構造器,直至 Object 類。這些構造器的調用者皆爲同一對象,也就是經過 new 指令新建而來的對象。

事實上,我上面的陳述意味着:經過 new 指令新建出來的對象,它的內存其實涵蓋了全部父類中的實例字段。

也就是說,雖然子類沒法訪問父類的私有實例字段,或者子類的實例字段隱藏了父類的同名實例字段,可是子類的實例仍是會爲這些父類實例字段分配內存的。

下面我將介紹壓縮指針技術。在 Java 虛擬機中,每一個 Java 對象都有一個對象頭,它由標記字段和類型指針所構成。

標記字段用以存儲 Java 虛擬機有關該對象的運行數據,如哈希碼、GC 信息以及鎖信息,而類型指針則指向該對象的類。

在64位的JVM中,對象頭的標記字段佔 64 位,而類型指針又佔了 64 位。也就是說,每個 Java 對象在內存中的額外開銷就是 16 個字節。

爲了儘可能較少對象的內存使用量,64位JVM引入了壓縮指針的概念,將堆中本來64位的Java對象指針壓縮成32位的。

這樣一來,對象頭中的類型指針也會被壓縮成32位,使得對象頭的大小從16字節降至12字節。

固然,壓縮指針不只能夠做用於對象頭的類型指針,還能夠做用於引用類型的字段,以及引用類型數組。

它的原理是什麼?答案是內存對齊

咱們規定,默認狀況下,JVM堆中對象的起始地址須要對齊至8的倍數,若是一個對象用不到8N 個字節,那麼空白的那部分空間就浪費掉了,這些浪費掉的空間咱們稱之爲對象間的填充。

你們知道,指針裏面存放的是地址,因爲堆中對象的起始地址是對齊至8的倍數,因此指針存放一個引用(或者對象的類)的內存地址時,根本就不用存放最後的三位二進制數。

由於全部對象或類的內存地址都對齊了8,因此他們的內存地址的最低三位老是0,32位的指針就能夠尋址到 2 的 35 次方個字節,也就是 32GB 的地址空間(超過 32GB 則會關閉壓縮指針)。

咱們能夠經過配置虛擬機的內存對齊選項來進一步提高尋址範圍。可是,這同時也可能增長對象間填充,致使壓縮指針沒有達到本來節省空間的效果。

就算是關閉了壓縮指針,Java 虛擬機仍是會進行內存對齊。此外,內存對齊不只存在於對象與對象之間,也存在於對象中的字段之間。

好比說,Java 虛擬機要求long字段、double字段,以及非壓縮指針狀態下的引用字段地址爲8的倍數。

這是爲何呢?

CPU的緩存行機制你們應該有所耳聞,若是字段不是對齊的,那麼就有可能出現跨緩存行的字段。

該字段的讀取可能須要替換兩個緩存行,而該字段的存儲也會同時污染兩個緩存行。

咱們將在後期文章關於volatile關鍵詞的本質分析的過程當中,再次考察到CPU緩存行的相關機制。

最後我要提一句的是,字段重排列技術,就是我剛纔提到的,對象的字段之間存在的內存對齊。這指的是從新分配字段的前後順序,以達到內存對齊的目的

它有如下兩個規則:

其一,若是一個字段佔據C個字節,那麼該字段的偏移量須要對齊至NC。這裏的偏移量指的是字段地址與對象的起始地址差值。

以Long類爲例,它僅有一個long類型的實例字段。在使用了壓縮指針的 64 位虛擬機中,儘管對象頭的大小爲12個字節,該 long 類型字段的偏移量也只能是16,而中間空着的4個字節便會被浪費掉。

其二,子類所繼承字段的偏移量,須要與父類對應字段的偏移量保持一致。

說白了,好比B繼承了A,A是B的父類,A中全部的字段,在B中都有,並且是先放A的字段,再放B的字段。並且B類對象放A類字段時,須要與父類對應字段的偏移量保持一致。

接下來我說一個拓展內容吧,什麼是虛共享?

假設兩個線程分別訪問同一對象中不一樣的 volatile 字段,邏輯上它們並無共享內容,所以不須要同步。

若是這兩個字段剛好在同一個緩存行中,那麼對這些字段的寫操做會致使緩存行的寫回,也就形成了實質上的共享。

Java8還引入了一個新的註釋@Contended,用來解決對象字段之間的虛共享。

Java 虛擬機會讓不一樣的@Contended字段處於獨立的緩存行中,所以你會看到大量的空間被浪費掉,避免無謂的緩存行同步操做。

具體的算法屬於實現細節了,你們有興趣能夠去用:

-XX:-RestrictContended

這個虛擬機選項,查看Contended字段的內存佈局。

END


我的公衆號:石杉的架構筆記(ID:shishan100)

歡迎長按下圖關注公衆號:石杉的架構筆記!

公衆號後臺回覆資料,獲取做者獨家祕製學習資料

石杉的架構筆記,BAT架構經驗傾囊相授

相關文章
相關標籤/搜索