最近在讀《深入理解Java虛擬機器》,對Java物件的記憶體佈局有了進一步的認識,於是腦子裡自然而然就有一個很普通的問題,就是一個Java物件到底佔用多大記憶體?
在網上搜到了一篇部落格講的非常好:http://yueyemaitian.iteye.com/blog/2033046,裡面提供的這個類也非常實用:
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; /** * 物件佔用位元組大小工具類 * * @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); } }
大家可以用這個程式碼邊看邊驗證,注意的是,執行這個程式需要通過javaagent注入Instrumentation,具體可以看原部落格。我今天主要是總結下手動計算Java物件佔用位元組數的基本規則,做為基本的技能必須get√,希望能幫到和我一樣的Java菜鳥。
在介紹之前,簡單回顧下,Java物件的記憶體佈局:物件頭(Header),例項資料(Instance Data)和對齊填充(Padding),詳細的可以看我的讀書筆記。另外:不同的環境結果可能有差異,我所在的環境是HotSpot虛擬機器,64位Windwos。
下面進入正文:
物件頭
物件頭在32位系統上佔用8bytes,64位系統上佔用16bytes。
例項資料
原生型別(primitive type)的記憶體佔用如下:
Primitive Type | Memory Required(bytes) |
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
reference型別在32位系統上每個佔用4bytes, 在64位系統上每個佔用8bytes。
對齊填充
HotSpot的對齊方式為8位元組對齊:
(物件頭 + 例項資料 + padding) % 8等於0且0 <= padding < 8
指標壓縮
物件佔用的記憶體大小收到VM引數UseCompressedOops的影響。
1)對物件頭的影響
開啟(-XX:+UseCompressedOops)物件頭大小為12bytes(64位機器)。
static class A { int a; }
A物件佔用記憶體情況:
關閉指標壓縮: 16+4=20不是8的倍數,所以+padding/4=24
開啟指標壓縮: 12+4=16已經是8的倍數了,不需要再padding。
1) 對reference型別的影響
64位機器上reference型別佔用8個位元組,開啟指標壓縮後佔用4個位元組。
static class B2 { int b2a; Integer b2b; }
B2物件佔用記憶體情況:
關閉指標壓縮: 16+4+8=28不是8的倍數,所以+padding/4=32
開啟指標壓縮: 12+4+4=20不是8的倍數,所以+padding/4=24
陣列物件
64位機器上,陣列物件的物件頭佔用24個位元組,啟用壓縮之後佔用16個位元組。之所以比普通物件佔用記憶體多是因為需要額外的空間儲存陣列的長度。
先考慮下new Integer[0]佔用的記憶體大小,長度為0,即是物件頭的大小:
未開啟壓縮:24bytes
開啟壓縮後:16bytes
接著計算new Integer[1],new Integer[2],new Integer[3]和new Integer[4]就很容易了:
未開啟壓縮:
開啟壓縮:
拿new Integer[3]來具體解釋下:
未開啟壓縮:24(物件頭)+8*3=48,不需要padding;
開啟壓縮:16(物件頭)+3*4=28,+padding/4=32,其他依次類推。
自定義類的陣列也是一樣的,比如:
static class B3 { int a; Integer b; }
new B3[3]佔用的記憶體大小:
未開啟壓縮:48
開啟壓縮後:32
複合物件
計算複合物件佔用記憶體的大小其實就是運用上面幾條規則,只是麻煩點。
1)物件本身的大小
直接計算當前物件佔用空間大小,包括當前類及超類的基本型別例項欄位大小、引用型別例項欄位引用大小、例項基本型別陣列總佔用空間、例項引用型別陣列引用本身佔用空間大小; 但是不包括超類繼承下來的和當前類宣告的例項引用欄位的物件本身的大小、例項引用陣列引用的物件本身的大小。
static class B { int a; int b; } static class C { int ba; B[] as = new B[3]; C() { for (int i = 0; i < as.length; i++) { as[i] = new B(); } } }
未開啟壓縮:16(物件頭)+4(ba)+8(as引用的大小)+padding/4=32
開啟壓縮:12+4+4+padding/4=24
2)當前物件佔用的空間總大小
遞迴計算當前物件佔用空間總大小,包括當前類和超類的例項欄位大小以及例項欄位引用物件大小。
遞迴計算複合物件佔用的記憶體的時候需要注意的是:對齊填充是以每個物件為單位進行的,看下面這個圖就很容易明白。
現在我們來手動計算下C物件佔用的全部記憶體是多少,主要是三部分構成:C物件本身的大小+陣列物件的大小+B物件的大小。
未開啟壓縮:
(16 + 4 + 8+4(padding)) + (24+ 8*3) +(16+8)*3 = 152bytes
開啟壓縮:
(12 + 4 + 4 +4(padding)) + (16 + 4*3 +4(陣列物件padding)) + (12+8+4(B物件padding))*3= 128bytes
大家有興趣的可以試試。
實際工作中真正需要手動計算物件大小的場景應該很少,但是個人覺得做為基礎知識每個Java開發人員都應該瞭解,另外:對自己寫的程式碼大概佔用多少記憶體,記憶體中是怎麼佈局的應該有一個直覺性的認識。