你寫的Java對象究竟佔多少內存?

Java 作爲一個面嚮對象語言,給我們帶來了多態,繼承,封裝等特性,使得我們可以利用這些特性很輕鬆的就能構建出易於擴展,易於維護的代碼。作爲一個Javaer,天天搞“對象”,那你寫的對象究竟佔用了多少內存呢?我們來看看你的“對象”是如何“敗家”的。

本文環境:jdk1.8_64

Java 對象頭內存模型
想要了解Java對象究竟佔用多少內存必定先要了解一個Java 對象的內存模型是怎麼樣的?由於我們的虛擬機是分爲32位和64位,那肯定它們的模型也是有區別的,下面我列出列32位虛擬機和64位虛擬機下的Java對象頭內存模型。
32位虛擬機
64位虛擬機
64位帶指針壓縮
因爲筆者的本地環境是jdk1.8,64位虛擬機,這裏我以64位虛擬機(開啓指針壓縮)來分析,因爲默認情況下,jdk1.8 在64位虛擬機默認開啓指針壓縮。
Java 對象頭主要包括兩部分,第一部分就是 Mark Word,這也是 Java 鎖實現原理中重要的一環,另外一部分是 Klass Word。
Klass Word 這裏其實是虛擬機設計的一個oop-klass model模型,這裏的OOP是指Ordinary Object Pointer(普通對象指針),看起來像個指針實際上是藏在指針裏的對象。而 klass 則包含 元數據和方法信息,用來描述 Java 類。它在64位虛擬機開啓壓縮指針的環境下佔用 32bits 空間。
Mark Word 是我們分析的重點,這裏也會設計到鎖的相關知識。Mark Word 在64位虛擬機環境下佔用 64bits 空間。整個Mark Word的分配有幾種情況:

未鎖定(Normal): 哈希碼(identity_hashcode)佔用31bits,分代年齡(age)佔用4 bits,偏向模式(biased_lock)佔用1 bits,鎖標記(lock)佔用2 bits,剩餘26bits 未使用(也就是全爲0)
可偏向(Biased): 線程id 佔54bits,epoch 佔2 bits,分代年齡(age)佔用4 bits,偏向模式(biased_lock)佔用1 bits,鎖標記(lock)佔用2 bits,剩餘 1bit 未使用。
輕量鎖定(Lightweight Locked): 鎖指針佔用62bits,鎖標記(lock)佔用2 bits。
重量級鎖定(Heavyweight Locked):鎖指針佔用62bits,鎖標記(lock)佔用2 bits。
GC 標記:標記位佔2bits,其餘爲空(也就是填充0)

以上就是我們對Java對象頭內存模型的解析,只要是Java對象,那麼就肯定會包括對象頭,也就是說這部分內存佔用是避免不了的。所以,在筆者64位虛擬機,Jdk1.8(開啓了指針壓縮)的環境下,任何一個對象,啥也不做,只要聲明一個類,那麼它的內存佔用就至少是96bits,也就是至少12字節。
驗證模型
我們來寫點代碼來驗證一下上述的內存模型,這裏推薦openjdk的jol工具,它可以幫助你查看對象內存的佔用情況。
首先添加maven依賴

org.openjdk.jol
jol-core
0.10

複製代碼我們先來看看,如果只是新建一個普通的類,什麼屬性也不添加,佔用的空間是多少?
/**

  • @description:
  • @author: luozhou
  • @create: 2020-02-26 10:00
    **/
    public class NullObject {

}
複製代碼按照我們之前的Java對象內存模型分析,一個空對象,那就是隻有一個對象頭部,在指針壓縮的條件下會佔用 96 bits,也就是12bytes。
運行工具查看空間佔用
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new NullObject()).toPrintable());
}
複製代碼上面這行代碼會解析你新建一個NullObject對象,佔用了多少內存。我們執行看看結果如何:
內存佔用
這裏我們發現結果顯示:Instance size:16 bytes,結果就是16字節,與我們之前預測的12字節不一樣,爲什麼會這樣呢?我們看到上圖中有3行 object header,每個佔用4字節,所以頭部就是12字節,這裏和我們的計算是一致的,最後一行是虛擬機填充的4字節,那爲什麼虛擬機要填充4個字節呢?
什麼是內存對齊
想要知道爲什麼虛擬機要填充4個字節,我們需要了解什麼是內存對齊?
我們程序員看內存是這樣的:

上圖表示一個坑一個蘿蔔的內存讀取方式。但實際上 CPU 並不會以一個一個字節去讀取和寫入內存。相反 CPU 讀取內存是一塊一塊讀取的,塊的大小可以爲 2、4、6、8、16 字節等大小。塊大小我們稱其爲內存訪問粒度。如下圖:

假設一個32位平臺的 CPU,那它就會以4字節爲粒度去讀取內存塊。那爲什麼需要內存對齊呢?主要有兩個原因:

平臺(移植性)原因:不是所有的硬件平臺都能夠訪問任意地址上的任意數據。例如:特定的硬件平臺只允許在特定地址獲取特定類型的數據,否則會導致異常情況。
性能原因:若訪問未對齊的內存,將會導致 CPU 進行兩次內存訪問,並且要花費額外的時鐘週期來處理對齊及運算。而本身就對齊的內存僅需要一次訪問就可以完成讀取動作。

我用圖例來說明 CPU 訪問非內存對齊的過程:

在上圖中,假設CPU 是一次讀取4字節,在這個連續的8字節的內存空間中,如果我的數據沒有對齊,存儲的內存塊在地址1,2,3,4中,那CPU的讀取就會需要進行兩次讀取,另外還有額外的計算操作:

CPU 首次讀取未對齊地址的第一個內存塊,讀取 0-3 字節。並移除不需要的字節 0。
CPU 再次讀取未對齊地址的第二個內存塊,讀取 4-7 字節。並移除不需要的字節 5、6、7 字節。
合併 1-4 字節的數據。
合併後放入寄存器。

所以,沒有進行內存對齊就會導致CPU進行額外的讀取操作,並且需要額外的計算。如果做了內存對齊,CPU可以直接從地址0開始讀取,一次就讀取到想要的數據,不需要進行額外讀取操作和運算操作,節省了運行時間。我們用了空間換時間,這就是爲什麼我們需要內存對齊。
回到Java空對象填充了4個字節的問題,因爲原字節頭是12字節,64位機器下,內存對齊的話就是128位,也就是16字節,所以我們還需要填充4個字節。
非空對象佔用內存計算
我們知道了一個空對象是佔用16字節,那麼一個非空對象究竟佔用多少字節呢?我們還是寫一個普通類來驗證下:
public class TestNotNull {
private NullObject nullObject=new NullObject();
private int a;
}
複製代碼這個演示類中引入了別的對象,我們知道int類型是佔用4個字節,NullObject對象佔用16字節,對象頭佔12字節,還有一個很重要的情況 NullObject在當前這個類中是一個引用,所以不會存真正的對象,而只存引用地址,引用地址佔4字節,所以總共就是12+4+4=20字節,內存對齊後就是24字節。我們來驗證下是不是這個結果:
public static void main(String[] args) {
//打印對象內存佔用
System.out.println(ClassLayout.parseInstance(new TestNotNull()).toPrintable());
System.out.println("=");
//輸出對象相關所有內存佔用
System.out.println(GraphLayout.parseInstance(new TestNotNull()).toPrintable());
System.out.println("
=");
//輸出內存佔用統計
System.out.println(GraphLayout.parseInstance(new TestNotNull()).toFootprint());
}
複製代碼結果如下:

我們可以看到TestNotNull的類佔用空間是24字節,其中頭部佔用12字節,變量a是int類型,佔用4字節,變量nullObject是引用,佔用了4字節,最後填充了4個字節,總共是24個字節,與我們之前的預測一致。但是,因爲我們實例化了NullObject,這個對象一會存在於內存中,所以我們還需要加上這個對象的內存佔用16字節,那總共就是24bytes+16bytes=40bytes。我們圖中最後的統計打印結果也是40字節,所以我們的分析正確。
這也是如何分析一個對象真正的佔用多少內存的思路,根據這個思路加上openJDK的jol工具就可以基本的掌握自己寫的“對象”究竟敗家了你多少內存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章