一看就懂的Java物件記憶體佈局

Java物件的記憶體佈局

1 前言

新建物件的方式:

Object。clone,反序列化直接複製已有資料,初始化新建物件的例項欄位

Unsafe。allocateInstance沒有初始化例項欄位

new

反射new和反射機制,都是透過呼叫構造器來初始化例項欄位

如new編譯而成的位元組碼包含:

請求記憶體的new指令

呼叫構造器的invokespecial指令

一看就懂的Java物件記憶體佈局

若一個類未定義任何構造器, 則Java編譯器會自動新增一個無參構造器:

一看就懂的Java物件記憶體佈局

但子類構造器需呼叫父類構造器:

若父類存在無參構造器,該呼叫可以是隱式,即Java編譯器會自動新增對父類構造器的呼叫

若父類沒有無參構造器,則子類構造器需顯式呼叫父類帶參構造器

顯式呼叫又可分為:

super關鍵字呼叫父類構造器

this關鍵字呼叫同一類中的其它構造器

都要作為構造器的第一條語句,以優先初始化繼承得的父類欄位。(但這也能透過呼叫其他生成引數的方法或位元組碼注入來繞開)

呼叫一個構造器時,優先呼叫父類構造器,直至Object類。這些構造器的呼叫者皆為同一物件,即透過new指令新建而來的物件。透過new指令新建的物件,其記憶體含所有父類例項欄位。雖子類無法訪問父類private例項欄位或子類的例項欄位隱藏了父類的同名例項欄位,但子類例項還是會為這些父類例項欄位分配記憶體。

那這些欄位在記憶體如何分佈的呢?

2 壓縮指標

JVM,每個Java物件都有物件頭(object header),由標記欄位和型別指標構成:

標記欄位,儲存JVM有關該物件的執行資料,如雜湊碼、GC資訊及鎖資訊

型別指標,指向該物件的類

64位JVM,物件頭的標記欄位佔64位,型別指標又佔64位。每個Java物件在記憶體中額外開銷就是16位元組。Integer類僅有一個int型別私有欄位,佔4位元組。因此,每個Integer物件額外記憶體開銷至少400%。這也是引入基本型別的原因之一。

為降低物件記憶體使用量,64位JVM引入壓縮指標[1](對應虛擬機器選項-XX:+UseCompressedOops,預設開啟),將堆中原本64位的Java物件指標壓縮成32位。

這樣,物件頭中的型別指標也會被壓縮成32位,使物件頭大小從16位元組降至12位元組。

2。1 作用範圍

物件頭的型別指標

引用型別的欄位

引用型別陣列

2。2 原理

路上全是房車,而且每輛房車恰好佔據兩個停車位。按序編號,停在0號和1號停車位上的叫0號車,停在2號和3號停車位上的叫1號車,依次類推。

原本記憶體定址用車位號。如我有一個值為6的指標,代表第6車位,沿這指標可找到3號車。

規定指標裡存的值是車號,如3指代3號車。當查詢3號,可將該指標值乘2,再沿著6號車位找到3號車。

這樣32位壓縮指標最多可標記2的32次方輛車,對應著2的33次方個車位。房車也有大小之分。大房車佔據車位可能是三個甚至是更多。不過這並不會影響定址演算法:只需跳過部分車號,便可保持原本車號*2的定址系統。

上述模型有一個前提,就是每輛車都從偶數號車位停起。這稱為記憶體對齊(對應虛擬機器選項-XX:ObjectAlignmentInBytes,預設值為8)。

預設Java虛擬機器堆中物件起始地址需對齊至8倍數。如一個物件用不到8N位元組,那空白部分空間就浪費了。這些浪費的空間稱為物件間的填充(padding)。

預設Java虛擬機器中32位壓縮指標可定址到2的35次方個位元組,即32GB地址空間(超過32GB則會關閉壓縮指標)。

在對壓縮指標解引用時,需將其左移3位,再加上一個固定偏移量,便可得到能定址32GB地址空間的偽64位指標。

可透過配置剛提到的記憶體對齊選項(-XX:ObjectAlignmentInBytes)進步提升定址範圍。但是,這同時也可能增加物件間填充,導致壓縮指標沒有達到原本節省空間的效果。

舉例來說,如規定每輛車都需從偶數車位號停起,那佔據兩個車位的小房車剛好,而對需三個車位的大房車僅是浪費一個車位。

但如規定需從4倍數號車位停起,那小房車則會浪費兩車位,而大房車至多可能浪費三車位。

就算關閉壓縮指標,Java虛擬機器還是會進行記憶體對齊。記憶體對齊不僅存在於物件與物件間,也存在物件中的欄位間。比如說,Java虛擬機器要求long欄位、double欄位及非壓縮指標狀態下的引用欄位地址為8的倍數。

欄位記憶體對齊的其中一個原因是讓欄位只出現在同一CPU的快取行。如欄位不對齊,可能出現跨快取行的欄位,即該欄位的讀取可能需替換兩個快取行,而該欄位的儲存也會同時汙染兩個快取行。這對程式執行效率都不好。

3 欄位重排列

JVM重新分配欄位先後順序,以達到記憶體對齊。Java虛擬機器中有三種排列方法(對應Java虛擬機器選項-XX:FieldsAllocationStyle,預設值為1),但都遵循如下規則:

如一個欄位佔C個位元組,那該欄位偏移量需對齊至NC偏移量指欄位地址與物件的起始地址差值。long類僅有一個long型別例項欄位。在使用壓縮指標的64位虛擬機器,儘管物件頭12位元組,該long型別欄位的偏移量也只能是16,而中間空著的4位元組便會被浪費掉。

子類所繼承欄位的偏移量,要與父類對應欄位的偏移量保持一致

JVM還會對齊子類欄位的起始位置:

使用壓縮指標的64位虛擬機器,子類第一個欄位需對齊至4N

關閉壓縮指標的64位虛擬機器,子類第一個欄位需對齊至8N

一看就懂的Java物件記憶體佈局

兩個類A和B,其中B繼承A。A和B各自定義一個long型別的例項欄位和一個int型別例項欄位。

B類在啟用壓縮指標和未啟用壓縮指標時,各欄位偏移量:

啟用壓縮指標,JVM將A類int欄位置於long欄位前,以填充因long欄位對齊造成的4位元組缺口。由於物件整體大小需對齊至8N,因此物件最後會有4位元組空白填充。

一看就懂的Java物件記憶體佈局

當關閉壓縮指標時,B類欄位起始位置需對齊至8N。那B類欄位前後各有4位元組空白。

Java 8還引入新註釋@Contended,解決物件欄位之間的虛共享(false sharing)問題[2]。也影響欄位排列。

4 虛共享

假設兩執行緒分別訪問同一物件中不同的volatile欄位,邏輯上無共享內容,因此無需同步。但若這倆欄位恰在同一快取行,則對這些欄位寫操作會導致快取行的寫回,即造成了實質上的共享。

Java虛擬機器會讓不同@Contended欄位處於獨立快取行,因此大量空間被浪費。可查閱Contended欄位的記憶體佈局。注意使用虛擬機器選項-XX:-RestrictContended。

5 總結

本文介紹JVM構造物件的方式,所構造物件的大小,以及物件的記憶體佈局。

new語句會被編譯為new指令及對構造器呼叫。每個類的構造器都會直接或者間接呼叫父類構造器,且在同一例項中初始化相應欄位。

JVM引入壓縮指標,將原本64位指標壓縮成32位。壓縮指標要求JVM堆中物件的起始地址要對齊至8倍數,還會對每個類的欄位進行重排列,使得欄位也記憶體對齊。

使用JOL工具列印你工程中的類的欄位分佈:

curl -L -O http://central。maven。org/maven2/org/openjdk/jol/jol-cli/0。9/jol-cli-0。9-full。jar java -cp jol-cli-0。9-full。jar org。openjdk。jol。Main internals java。lang。String

[1] https://wiki。openjdk。java。net/display/HotSpot/CompressedOops

[2] http://openjdk。java。net/jeps/142