最近在讀《深入理解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)的記憶體佔用如下:
itive 類型 Memory Required(bytes) 1
short 2
char 2
int 4
float 4
long 8
double 8
tes, referenceence在32位元系統上佔用每個位元系統所佔用每個位元系統類型。 對齊填充 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
1
11 開啟指標已經是8的倍數了,不需要再padding。2) 對reference類型的影響
64位元機器上reference類型佔用8個位元組,開啟4個位元組壓縮後佔用4個位元組。static class B2 { int b2a; Integer b2b; }
B2物件佔用記憶體狀況:
關閉指標壓縮: 16+4+8=28不是8的倍數,所以+padding/4=32 開啟指標
1 =20不是8的倍數,所以+padding/4=24
數組物件64位元機器上,陣列物件的物件頭佔用24個位元組,啟用壓縮之後佔用16個位元組。之所以比一般物件佔用記憶體多是因為需要額外的空間儲存數組的長度。
先考慮下new Integer[0]佔用的記憶體大小,長度為0,也就是物件頭的大小: 未開啟壓縮:24bytes1
接著計算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开发人员都应该了解,另外:对自己写的代码大概占用多少内存,内存中是怎么布局的应该有一个直觉性的认识。