一個Java物件到底佔用多大記憶體?

Nauyus發表於2019-12-17

在進行 JVM 調優時,我們經常關注 JVM 各個區域大小以及相關引數,從而進行特定的優化,在一次排查記憶體溢位問題時我不禁想到一個問題,一個 Java 物件到底佔用多大記憶體?下面我們就來分析驗證下。

Java 物件記憶體結構

在 JVM 中,Java 物件都是在堆記憶體上分配的,想要分析出 Java 物件記憶體佔用,首先要了解 Java 物件記憶體結構,一個 Java 物件記憶體佔用由三部分組成:物件頭(Header),例項資料(Instance Data)對齊填充(Padding)

物件頭(Header)

物件頭的組成

虛擬機器的物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料,如 hashCodeGC分代年齡鎖狀態標誌執行緒持有的鎖偏向執行緒ID偏向時間戳等。這部分資料的長度在 32 位和 64 位的虛擬機器(未開啟指標壓縮)中分別為 4B 和 8B ,官方稱之為 ”Mark Word”

物件的另一部分是型別指標(kclass),即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是那個類的例項。另外如果物件是一個 Java 陣列,那在物件頭中還必須有一塊用於記錄陣列長度的資料,因為虛擬機器可以通過普通 Java 物件的後設資料資訊確定 Java 物件的大小,但是從陣列的後設資料中卻無法確定陣列的大小。同樣,這部分資料的長度在 32 位和 64 的虛擬機器(未開啟指標壓縮)中分別為 4B 和 8B。

指標壓縮

從 JDK 1.6 update14 開始,64 bit JVM 正式支援了 -XX:+UseCompressedOops 這個可以壓縮指標,起到節約記憶體佔用的新引數。

如果 UseCompressedOops 是開啟的,則以下物件的指標會被壓縮:

所有物件的 klass 屬性
所有物件指標例項的屬性
所有物件指標陣列的元素(objArray)
複製程式碼

由此我們可以計算出物件頭大小:

32位虛擬機器物件頭大小= Mark Word(4B)+ kclass(4B) = 8B   
64位虛擬機器物件頭大小= Mark Word(8B)+ kclass(4B) = 12B
複製程式碼

例項資料

一個 Java 物件中的例項資料可能包括兩種,一是 8 種基本型別,二是例項資料也是一個物件,說到這裡很多人可能有個誤區:

基本型別?基本型別不是在棧上分配記憶體的嗎?怎麼要計算到分配在堆記憶體上物件的大小裡面去?

基本型別在棧上分配記憶體?其實並不是,所謂“棧記憶體儲存基本型別以及物件的引用(reference),堆記憶體儲存物件” 只是一句不嚴謹的話,實際仔細研究起來,棧記憶體(更專業的術語叫做堆疊)作為虛擬機器作為方法呼叫和方法執行的資料結構,可能儲存五種資訊:

區域性變數表
運算元棧
動態連結
方法返回地址
附加資訊
複製程式碼

其中區域性變數表中儲存了方法中的區域性變數,可能為 8 種基本型別或者 reference

也就是說,棧記憶體中儲存的基本型別,都是方法中的區域性變數,而如果基本型別作為物件的例項變數,是在堆上分配空間的,此外,如果例項變數被final修飾,則既不在棧也不在堆上分配空間,而是分配到常量池裡面。

8 種基本型別和 reference 大小在虛擬機器上都是固定的,見下表

Primitive Type Memory Required(bytes)
boolean 1
byte 1
short 2
char 2
int 4
float 4
long 8
double 8
Reference 4

對齊填充(Padding)

由於虛擬機器記憶體管理體系要求 Java 物件記憶體起始地址必須為 8 的整數倍,換句話說,Java 物件大小必須為 8 的整數倍,當物件頭+例項資料大小不為 8 的整數倍時,將會使用Padding機制進行填充,譬如, 64 位虛擬機器上 new Object() 實際大小為:

Mark Word(8B)+ kclass(4B)[開啟指標壓縮] = 12B

但由於Padding機制,實際佔用空間為: Mark Word(8B)+ kclass(4B)[開啟指標壓縮]+Padding(4B) = 16B

陣列的大小

Java 中陣列也是一種物件,陣列的大小與普通 Java 物件相比多了陣列長度的資訊(4B),即一個陣列物件大小為 Mark Word(8B)+ kclass(4B)[開啟指標壓縮] + 陣列長度(4B) = 16B

使用Instrumentation計算 Java 物件大小

現在我們已經知道了一個 Java 物件的大小 = 物件頭 + 例項資料 + Padding ,現在,我們驗證一下計算結果,google 到一個 Instrumentation 剛好可以計算物件大小

Instrumentation 是 Java SE 5 引入的特性,使用 Instrumentation,開發者可以構建一個獨立於應用程式的代理程式(Agent),用來監測和協助執行在 JVM 上的程式,甚至能夠實現位元組碼修改技術。簡單的說,Instrumentation 實現了一個虛擬機器層面的 AOP 。

本文不涉及 Instrumentation 的複雜應用,我們只使用 Instrumentation 其中一個 getObjectSize() 方法獲取物件大小。

使用 Instrumentation 需要使用 javaagent 技術, 簡單說就是執行一個帶 main 函式的類時可以通過 –javaagent 引數指定一個特定的 jar 檔案(包含 Instrumentation 代理)來啟動 Instrumentation 的代理程式。具體分為三步:

一 編寫一個Instrumentation類作為代理

其中 premain 注入 Instrumentation ,sizeOf 用來計算物件佔用空間

ObjectShallowSize.java:

package sizeof;
 
import java.lang.instrument.Instrumentation;
 
public class ObjectShallowSize {
	private static Instrumentation inst;
	
	public static void premain(String agentArgs, Instrumentation instP){
		inst = instP;
	}
	
	public static long sizeOf(Object obj){
		return inst.getObjectSize(obj);
	}
}
複製程式碼

二 打包

在 ObjectShallowSize.java 路徑下新建 /META-INF/MANIFEST.MF 指定 Premain-Class 內容為:

Manifest-Version: 1.0
Premain-Class: sizeof.ObjectShallowSize
複製程式碼

然後編譯,打包

javac -d . ObjectShallowSize.java
jar cvfm java-agent-sizeof.jar META-INF/MANIFEST.MF  .
複製程式碼

三 執行

編寫一個測試模版類 ObjectSizeTest.java ,使用

java -javaagent:java-agent-sizeof.jar ObjectSizeTest
複製程式碼

來執行程式

ObjectSizeTest.java 程式碼如下:

package sizeof;
public class ObjectSizeTest {

    public static void main(String[] args) {
        System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest));
    }
}
複製程式碼

ObjectSizeTest 沒有例項變數,理論計算

ObjectSizeTest大小 = Mark Word(8B)+ kclass(4B) [開啟指標壓縮]+Padding(4B) = 16B

為了方便,我們在 IDEA 中驗證一下,匯入剛才的 ObjectSizeTest 類,指定 JVM 引數如圖

一個Java物件到底佔用多大記憶體?

執行結果為 16B,和我們猜想一致

一個Java物件到底佔用多大記憶體?

接下來我們在模版類中新增幾個例項變數驗證下

package sizeof;

public class ObjectSizeTest {

    private int i;

    public static void main(String[] args) {
        System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
    }
}

複製程式碼

理論值:Mark Word(8B)+ kclass(4B) + i(4B) = 16B

實際值:16B

package sizeof;

public class ObjectSizeTest {

    private int i;
    private int j;

    public static void main(String[] args) {
        System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
    }
}
複製程式碼

理論值:Mark Word(8B)+ kclass(4B) + i(4B) + j(4B)+Padding(4B) = 24B

實際值:24B

package sizeof;

public class ObjectSizeTest {

    private int i;
    private int j;
    private String s;
    private boolean aBoolean;
    private char c;
    

    public static void main(String[] args) {
        System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
    }
}
複製程式碼

理論值:Mark Word(8B)+ kclass(4B) + i(4B) + j(4B) + s(4B) + aBoolean(1B) + c(2B) + Paddding(5B) = 32B

實際值:32B

package sizeof;

public class ObjectSizeTest {

    private String s; // 4
    private int i1; // 4
    private byte b1; // 1
    private byte b2; // 1
    private int i2;// 4
    private Object obj; //4
    private byte b3;  // 1


    public static void main(String[] args) {
        System.out.println(ObjectShallowSize.sizeOf(new ObjectSizeTest()));
    }
}
複製程式碼

理論值:Mark Word(8B)+ kclass(4B) + s(4B) + i1(4B) + b1(1B) + b2(1B) + 2(padding) + i2(4B) + obj(4B)+ b3(1B) + Paddding(7B) = 40B

實際值:32B

納尼?這裡為什麼理論值和實際值不一致?

事實上,HotSpot建立的物件的欄位會先按照給定順序排列一下,預設的順序如下,從長到短排列,引用排最後: long/double --> int/float --> short/char --> byte/boolean --> Reference

這個順序可以使用JVM引數: -XX:FieldsAllocationSylte = 0 (預設是1)來改變。

按照這種方法我們來重新計算下物件大小

Mark Word(8B)+ kclass(4B) + i1(4B) + i2(4B) + b1(1B) + b2(1B) + b3(1B) + Paddding(1B) + s(4B) + obj(4B) = 32B

與預期值一致。

Java物件實際大小

前面我們計算 Java 物件大小時,對於例項變數為物件的,只計算了其reference的大小,實際應該也將例項變數本身計算在內,我們可以通過反射機制取出 Java 物件中例項變數,遞迴計算累加出實際大小。 yueyemaitian.iteye.com/blog/203304… 已經提供了現成的程式如下,使用fullSizeOf()方法即可計算出 Java 物件實際大小。

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;

/**
 * 物件佔用位元組大小工具類
 * <p>
 *
 * @author tianmai.fh
 * @date 2014-03-18 11:29
 */
public class SizeOfObject {
    static Instrumentation inst;

    public static void premain(String args, Instrumentation instP) {
        inst = instP;
    }

    /**
     * 直接計算當前物件佔用空間大小,包括當前類及超類的基本型別例項欄位大小、<br></br>
     * 引用型別例項欄位引用大小、例項基本型別陣列總佔用空間、例項引用型別陣列引用本身佔用空間大小;<br></br>
     * 但是不包括超類繼承下來的和當前類宣告的例項引用欄位的物件本身的大小、例項引用陣列引用的物件本身的大小 <br></br>
     *
     * @param obj
     * @return
     */
    public static long sizeOf(Object obj) {
        return inst.getObjectSize(obj);
    }

    /**
     * 遞迴計算當前物件佔用空間總大小,包括當前類和超類的例項欄位大小以及例項欄位引用物件大小
     *
     * @param objP
     * @return
     * @throws IllegalAccessException
     */
    public static long fullSizeOf(Object objP) throws IllegalAccessException {
        Set<Object> visited = new HashSet<Object>();
        Deque<Object> toBeQueue = new ArrayDeque<Object>();
        toBeQueue.add(objP);
        long size = 0L;
        while (toBeQueue.size() > 0) {
            Object obj = toBeQueue.poll();
            //sizeOf的時候已經計基本型別和引用的長度,包括陣列  
            size += skipObject(visited, obj) ? 0L : sizeOf(obj);
            Class<?> tmpObjClass = obj.getClass();
            if (tmpObjClass.isArray()) {
                //[I , [F 基本型別名字長度是2  
                if (tmpObjClass.getName().length() > 2) {
                    for (int i = 0, len = Array.getLength(obj); i < len; i++) {
                        Object tmp = Array.get(obj, i);
                        if (tmp != null) {
                            //非基本型別需要深度遍歷其物件  
                            toBeQueue.add(Array.get(obj, i));
                        }
                    }
                }
            } else {
                while (tmpObjClass != null) {
                    Field[] fields = tmpObjClass.getDeclaredFields();
                    for (Field field : fields) {
                        if (Modifier.isStatic(field.getModifiers())   //靜態不計  
                                || field.getType().isPrimitive()) {    //基本型別不重複計  
                            continue;
                        }

                        field.setAccessible(true);
                        Object fieldValue = field.get(obj);
                        if (fieldValue == null) {
                            continue;
                        }
                        toBeQueue.add(fieldValue);
                    }
                    tmpObjClass = tmpObjClass.getSuperclass();
                }
            }
        }
        return size;
    }

    /**
     * String.intern的物件不計;計算過的不計,也避免死迴圈
     *
     * @param visited
     * @param obj
     * @return
     */
    static boolean skipObject(Set<Object> visited, Object obj) {
        if (obj instanceof String && obj == ((String) obj).intern()) {
            return true;
        }
        return visited.contains(obj);
    }
}
複製程式碼

參考資料:

  • 《深入理解Java虛擬機器》

感謝閱讀,原創不易,如有啟發,點個贊吧!這將是我寫作的最強動力!本文不同步釋出於不止於技術的技術公眾號 Nauyus ,主要分享一些程式語言,架構設計,思維認知類文章, 2019年12月起開啟周更模式,歡迎關注,共同學習成長!

一個Java物件到底佔用多大記憶體?

相關文章