JVM —— Java 對象佔用空間大小計算
零. 為什麼要知道 Java 對象佔用空間大小
- 緩衝的實現: 在設計 JVM 內緩衝時(不是藉助 Memcached、 Redis 等), 需要知道緩衝的對象是否會超過 JVM 最大堆限制, 如果會超過要設定相應演算法如 LRU 來丟棄一部分快取資料以滿足後續內容的緩衝JVM 參數設定: 如果知道對象會被建立, 可以協助判斷 -Xmx 需要設定多少只是為了好玩 一. 對象的記憶體布局HotSpot 虛擬機器中,對象在記憶體中儲存的布局可以分為三塊地區:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)。 二. 對象頭JVM 對象頭一般佔用兩個機器碼,在 32-bit JVM 上佔用 64bit, 在 64-bit JVM 上佔用 128bit 即 8+8=16 bytes(開啟指標壓縮後佔用 4+8=12 bytes) 64位機器上,數組對象的對象頭佔用 24 bytes,啟用壓縮之後佔用 16 bytes。之所以比普通對象佔用記憶體多是因為需要額外的空間儲存數組的長度。 三. 執行個體資料
原生類型(primitive type)的記憶體佔用如下:
Primitive Type |
Memory Required(bytes) |
boolean |
1 |
byte |
1 |
short |
2 |
char |
2 |
int |
4 |
float |
4 |
long |
8 |
double |
8 |
參考型別(reference type: Integer)在 32 位系統上每個佔用 4bytes(即32bit, 才能管理 2^32=4G 的記憶體), 在 64 位元系統上每個佔用 8bytes(開啟壓縮為 4 bytes)。 四. 對齊填充HotSpot 的對齊為 8 位元組對齊,不足的需要 Padding 填充對齊, 公式:(對象頭 + 執行個體資料 + padding)% 8 == 0 (0<= padding <8) 五. 計算 Java 對象佔用空間大小藉助 Instrument 介面的 getObjectSize 方法計算對象佔用空間SizeOfAgent: 計算對象大小類
package com.wenniuwuren.objectsizeof;import java.lang.instrument.Instrumentation;import java.lang.reflect.Array;import java.lang.reflect.Field;import java.lang.reflect.Modifier;import java.util.IdentityHashMap;import java.util.Map;import java.util.Stack;/** * 藉助 Instrumentation 介面的 getObjectSize 方法計算對象佔用空間 * 原來的 sizeOf 只能計算本對象佔用空間, 無法計算繼承下來的佔用空間, * 不過可以用反射的方法把全部佔用空間計算出來 * * Created by zhuyb on 16/3/20. */public class SizeOfAgent { static Instrumentation instrumentation; // 第一個參數由 –javaagent, 第二個參數由 JVM 傳入 public static void premain(String agentArgs, Instrumentation instP) { instrumentation = instP; } // 返回沒有子類對象大小的大小 public static long sizeOf(Object o) { if (instrumentation == null) { throw new IllegalStateException("Can not access instrumentation environment.\n" + "Please check if jar file containing SizeOfAgent class is \n" + "specified in the java's \"-javaagent\" command line argument."); } return instrumentation.getObjectSize(o); } /** * * 計算綜合物件 * @param obj object to calculate size of * @return object size */ public static long fullSizeOf(Object obj) { Map visited = new IdentityHashMap(); Stack stack = new Stack(); long result = internalSizeOf(obj, stack, visited); while (!stack.isEmpty()) { result += internalSizeOf(stack.pop(), stack, visited); } visited.clear(); return result; } // 這個演算法使每個對象僅被計算一次, 避免循環參考,即死迴圈計算 private static boolean skipObject(Object obj, Map visited) { if (obj instanceof String) { // String 池裡已有的不再計算 if (obj == ((String) obj).intern()) { return true; } } return (obj == null) // 已有對象不再計算 || visited.containsKey(obj); } private static long internalSizeOf(Object obj, Stack stack, Map visited) { if (skipObject(obj, visited)){ return 0; } visited.put(obj, null); long result = 0; // get size of object + primitive variables + member pointers result += SizeOfAgent.sizeOf(obj); // 處理所有數組內容 Class clazz = obj.getClass(); if (clazz.isArray()) { // [I , [F 基本類型名字長度是2 if(clazz.getName().length() != 2) {// skip primitive type array int length = Array.getLength(obj); for (int i = 0; i < length; i++) { stack.add(Array.get(obj, i)); } } return result; } // 處理對象的所有欄位 while (clazz != null) { Field[] fields = clazz.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { // 不重複計算靜態類型欄位 if (!Modifier.isStatic(fields[i].getModifiers())) { // 不重複計算原始類型欄位 if (fields[i].getType().isPrimitive()) { continue; } else { // 使 private 屬性可訪問 fields[i].setAccessible(true); try { // objects to be estimated are put to stack Object objectToAdd = fields[i].get(obj); if (objectToAdd != null) { stack.add(objectToAdd); } } catch (IllegalAccessException ex) { assert false; } } } } clazz = clazz.getSuperclass(); } return result; }}
使用上述代碼必須將上述代碼打成 jar 包, 並且 MANIFEST.MF 檔案設定參數(Premain-Class:sizeof.agent.SizeOfAgentBoot-Class-Path:
Can-Redefine-Classes:false) 如果使用 Maven 打包的話, 可以直接在 pom.xml 裡面設定 MANIFEST.MF 的參數 : maven-jar-plugin 2.4 SizeOfAgent com.wenniuwuren.objectsizeof.SizeOfAgent false false
測試類別: SizeOfAgentTest package com.wenniuwuren.objectsizeof;import static com.wenniuwuren.objectsizeof.SizeOfAgent.*;/** * 以下結果在 64-bit JVM 下測試 * 啟動參數1(不壓縮指標長度):-javaagent:target/SizeOfAgent.jar -XX:-UseCompressedOops * * Created by zhuyb on 16/3/20. */public class SizeOfAgentTest { public static void main(String[] args) { System.out.println("------------------Null 物件----------------------------"); // 16 bytes + 0 + 0 = 16 Null 物件, 只有對象頭 System.out.println("sizeOf(new Object()) = " + sizeOf(new Object())); System.out.println("fullSizeOf(new Object()) = " + fullSizeOf(new Object())); System.out.println("----------------非Null 物件含有原始類型、參考型別------------------------------"); // 16 bytes + 8 + 4 + padding = 32 System.out.println("sizeOf(new A()) = " + sizeOf(new A())); System.out.println("fullSizeOf(new A()) = " + fullSizeOf(new A())); // 16 + 4 + padding =24 資料是一個 int System.out.println("sizeOf(new Integer(1)) = " + sizeOf(new Integer(1))); // (16 + int hash:4 + int hash32:4 + refer char value[]:8 + padding) = 32 // 靜態屬性(static)不計算空間,因為所有對象都是共用一塊空間的 // 不同版本JDK可能 String 內部 Field 可能不同,本次測試使用JDK1.7 System.out.println("sizeOf(new String()) = " + sizeOf(new String())); // (16 + 4 + 4 + 8 + padding) + (24 + 0 + padding) = 56 System.out.println("fullSizeOf(new String()) = " + fullSizeOf(new String())); // (16 + 4 + 4 + 8 + padding) = 32 System.out.println("sizeOf(new String('a')) = " + sizeOf(new String("a"))); // (16 + 4 + 4 + 8 +padding) + (24 + 2 + padding) = 64 System.out.println("fullSizeOf(new String('a')) = " + fullSizeOf(new String("a"))); System.out.println("-------------------原始類型數組對象---------------------------"); // 24 bytes + 0*1 + 0 = 24 數組長度為 0,所以只有對象頭的長度 System.out.println("sizeOf(new byte[0]) = " + sizeOf(new byte[0])); System.out.println("fullSizeOf(new byte[0]) = " + fullSizeOf(new byte[0])); // 24 + 1*1 + padding = 32 System.out.println("sizeOf(new byte[1]) = " + sizeOf(new byte[1])); System.out.println("fullSizeOf(new byte[1]) = " + fullSizeOf(new byte[1])); // 24 + 1*2 + padding = 32 System.out.println("sizeOf(new char[1]) = " + sizeOf(new char[1])); System.out.println("fullSizeOf(new char[1]) = " + fullSizeOf(new char[1])); // 24 + 9*1 + padding = 40 System.out.println("sizeOf(new byte[9]) = " + sizeOf(new byte[9])); System.out.println("fullSizeOf(new byte[9]) = " + fullSizeOf(new byte[9])); System.out.println("--------------------參考型別數組對象--------------------------"); // 24 bytes + 0*8 + 0 = 24 數組長度為 0 System.out.println("sizeOf(new Integer[0]) = " + sizeOf(new Integer[0])); System.out.println("fullSizeOf(new Integer[0]) = " + fullSizeOf(new Integer[0])); // 24 bytes + 1*8 + 0 = 32 引用對象 64-bit JVM 佔用 8 bytes System.out.println("sizeOf(new Integer[1]) = " + sizeOf(new Integer[1])); System.out.println("fullSizeOf(new Integer[1]) = " + fullSizeOf(new Integer[1])); // 24 bytes + 2*8 + padding = 40 System.out.println("sizeOf(new Integer[1]) = " + sizeOf(new Integer[1])); System.out.println("fullSizeOf(new Integer[1]) = " + fullSizeOf(new Integer[1])); // 24 + 3*8 + padding = 48 System.out.println("sizeOf(new Integer[3]) = " + sizeOf(new Integer[3])); System.out.println("fullSizeOf(new Integer[3]) = " + fullSizeOf(new Integer[3])); System.out.println("-------------------自訂數組對象---------------------------"); // 16 + (4+8) + padding = 32 System.out.println("sizeOf(new B()) = " + sizeOf(new B())); System.out.println("fullSizeOf(new B()) = " + fullSizeOf(new B())); // 24 + 0*8 + padding = 24 引用對象 64-bit JVM 佔用 8 bytes, // 因為沒建立真實的 new B()所以 B類內部資料還未佔用空間 System.out.println("sizeOf(new B[0]) = " + sizeOf(new B[0])); System.out.println("fullSizeOf(new B[0]) = " + fullSizeOf(new B[0])); // 24 + 1*8 + padding = 32 System.out.println("sizeOf(new B[1]) = " + sizeOf(new B[1])); System.out.println("fullSizeOf(new B[1]) = " + fullSizeOf(new B[1])); // 24 + 2*8 + padding = 40 System.out.println("sizeOf(new B[2]) = " + sizeOf(new B[2])); System.out.println("fullSizeOf(new B[2]) = " + fullSizeOf(new B[2])); // 24 + 3*8 + padding = 48 System.out.println("sizeOf(new B[3]) = " + sizeOf(new B[3])); System.out.println("fullSizeOf(new B[3]) = " + fullSizeOf(new B[3])); System.out.println("-------------------綜合物件---------------------------"); // 16 + (4+8) + padding = 32 sizeOf 只計算單層次佔用空間大小 System.out.println("sizeOf(new C()) = " + sizeOf(new C())); // (16 + (4+8) + padding1) + (24 + 2*8 + padding2) + 2*(16 + (4+8) + padding3) = 136 // 遞迴計算當前對象佔用空間總大小,包括當前類和超類的執行個體欄位大小以及執行個體欄位引用對象大小 System.out.println("fullSizeOf(new C()) = " + fullSizeOf(new C())); System.out.println("-------------------繼承關係---------------------------"); // 涉及繼承關係的時候有一個最基本的規則:首先存放父類中的成員,接著才是子類中的成員, 父類也要按照 8 byte 規定 // 16 + 1 + padding = 24 System.out.println("sizeOf(new D()) = " + sizeOf(new D())); System.out.println("fullSizeOf(new D()) = " + fullSizeOf(new D())); // 16 + 父類(1 + padding1) + 1 + padding2 = 32 System.out.println("sizeOf(new E()) = " + sizeOf(new E())); System.out.println("fullSizeOf(new E()) = " + fullSizeOf(new E())); } public static class A { int a; Integer b; } public static class B { int a; Integer b; } public static class C{ int c; B[] b = new B[2]; // 初始化 C() { for (int i = 0; i < b.length; i++) { b[i] = new B(); } } } public static class D { byte d1; } public static class E extends D { byte e1; }}
運行:如果在 IDE 運行時需要設定 JVM 參數:-javaagent:target/SizeOfAgent.jar -XX:-UseCompressedOops;如果在命令列運行命令:java -javaagent:sizeofag.jar -XX:-UseCompressedOops 主類名稱。測試結果:------------------Null 物件----------------------------sizeOf(new Object()) = 16fullSizeOf(new Object()) = 16----------------非Null 物件含有原始類型、參考型別------------------------------sizeOf(new A()) = 32fullSizeOf(new A()) = 32sizeOf(new Integer(1)) = 24sizeOf(new String()) = 32fullSizeOf(new String()) = 56sizeOf(new String('a')) = 32fullSizeOf(new String('a')) = 64-------------------原始類型數組對象---------------------------sizeOf(new byte[0]) = 24fullSizeOf(new byte[0]) = 24sizeOf(new byte[1]) = 32fullSizeOf(new byte[1]) = 32sizeOf(new char[1]) = 32fullSizeOf(new char[1]) = 32sizeOf(new byte[9]) = 40fullSizeOf(new byte[9]) = 40--------------------參考型別數組對象--------------------------sizeOf(new Integer[0]) = 24fullSizeOf(new Integer[0]) = 24sizeOf(new Integer[1]) = 32fullSizeOf(new Integer[1]) = 32sizeOf(new Integer[1]) = 32fullSizeOf(new Integer[1]) = 32sizeOf(new Integer[3]) = 48fullSizeOf(new Integer[3]) = 48-------------------自訂數組對象---------------------------sizeOf(new B()) = 32fullSizeOf(new B()) = 32sizeOf(new B[0]) = 24fullSizeOf(new B[0]) = 24sizeOf(new B[1]) = 32fullSizeOf(new B[1]) = 32sizeOf(new B[2]) = 40fullSizeOf(new B[2]) = 40sizeOf(new B[3]) = 48fullSizeOf(new B[3]) = 48-------------------綜合物件---------------------------sizeOf(new C()) = 48fullSizeOf(new C()) = 152-------------------繼承關係---------------------------sizeOf(new D()) = 24fullSizeOf(new D()) = 24sizeOf(new E()) = 32fullSizeOf(new E()) = 32
測試類別中綜合物件計算可能較為麻煩, 可以參照較為清楚地看出 new C() 的佔用空間計算: 六. 總結整體的 Java 對象是按照一定規則進行的, 清楚了 JVM 對象的記憶體布局和分配規則, 計算 Java 對象的大小就比較簡單了。Java 不像 C++ 可以提供對象大小, 這是 Java 語言的設計初衷(自動記憶體管理), 但是隨著對 Java 的深入瞭解, 又到了對 JVM (使用 C、C++ 實現) 底層實現的問題上。 本文的參考資料為 2007 年的, 至今已有 9 年, 參考資料內容至今還是有效,JVM 相關的東西變動確實小,挺有意思的