最近突發奇想,忽然對Java物件的記憶體大小感興趣,去網路上蒐集了一些資料,並且做一下整理,希望能夠各位幫助。
如果:你能算出new String(“abc”)這個物件在JVM中佔用記憶體大小(64位元JDK7中壓縮大小48B,未壓縮大小64B), 那麼看到這裡就可以結束了~
Java物件的記憶體佈局:物件頭(Header),實例資料(Instance Data)和對齊填充(Padding)。
虛擬機的物件頭包括兩部分訊息,第一部分用於儲存物件本身的執行時間資料,如hashCode、GC分代年齡、鎖定狀態標誌、執行緒持有的鎖定、偏向線程ID、偏向時間戳等。這部分資料的長度在32位元和64的虛擬機器(未開啟指標壓縮)中分別為4B和8B,官方稱之為」Mark Word」。
物件的另一部分是類型指標(klass),也就是物件指向它的類別元資料的指針,虛擬機器透過這個指標來決定這個物件是那個類別的實例。另外如果物件是一個Java數組,那麼再物件頭中還必須有一塊用於記錄數組長度的數據,因為虛擬機器可以透過普通Java物件的元資料資訊來確定Java物件的大小,但是從數組的元資料中卻無法確定數組的大小。
物件頭在32位元系統上佔用8B,64位元系統上佔16B。 無論是32位元系統還是64位元系統,物件都採用8位元組對齊。 Java在64位元模式下開啟指標壓縮,比32位元模式下,頭部會大4B(mark區域變位8B,kclass區域被壓縮),如果沒有開啟指標壓縮,頭部會大8B(mark和kclass都是8B),換句話說,
HotSpot的對齊方式為8位元組對齊:(物件頭+實例資料+padding)%8 等於0 且0<=padding<8。以下說明都是以HotSpot為基準。
在參考資料2中提到,再JDK5之後提供的java.lang.instrument.Instrumentation提供了豐富的對結構的等各方面的追蹤和物件大小的測量API。但是這個東西需要採用java的agent代理才能使用,至於agent代理和Instrumentation這裡就不闡述了,我這裡只闡述其使用方式。
在參考資料3中提供了這個類,個人覺得很實用,程式碼如下所附1所示(程式碼比較長,索性就放到文章最後了):
這段程式碼可以直接拷貝,然後將其打成jar包(命名為agent.jar,如果沒有打包成功,可以直接下載博主打包好的),注意在META-INF/MANIFEST.MF中添加一行:
Premain-Class: com.zzh.size.MySizeOf (注意":"后面的空格,否则会报错:invalid header field.)
舉個案例,程式碼如下(部落客的系統是64位元的,採用的是64位元的JDK7):
import com.zzh.size.MySizeOf;public class ObjectSize { public static void main(String args[]) { System.out.println(MySizeOf.sizeOf(new Object())); } }
接下來進行編譯運行,步驟如下:
編譯(agent.jar放在目前目錄下):javac -classpath agent.jar ObjectSize.java
執行:java -javaagent:agent.jar ObjectSize(輸出結果:16,至於這個結果的分析,稍後再闡述)
JDK6推出參數-XX:+UseCompressedOops,在32G記憶體一下預設會自動開啟這個參數。可以在運作參數中加入-XX:-UseCompressedOops來關閉指標壓縮。
使用Instrumentation來測試物件的大小,只是為了更形象的表示一個物件的大小,實際上當一個物件建立起來的時候可以手動計算其大小,程式碼案例實踐用來證明理論知識的合理性及正確性,具體演算法在下面的程式碼案例中有所體現。
補充:原生型別(primitive type)的記憶體佔用如下:
Primitive Type | Memory Required(bytes) |
---|---|
boolean | 1 |
#byte | ##1|
2 | |
#2 | |
#4 | |
4 | |
8 | |
引用类型在32位系统上每个占用4B, 在64位系统上每个占用8B。
案例分析
扯了这么多犊子,估计看的玄乎玄乎的,来几段代码案例来实践一下。
案例1:上面的new Object()的大小为16B,这里再重申一下,博主测试机是64位的JDK7,如无特殊说明,默认开启指针压缩。
new Object()的大小=对象头12B(8Bmak区,4Bkclass区)+padding的4B=16B
案例2:
static class A{ int a; } static class B{ int a; int b; } public static void main(String args[]) { System.out.println(MySizeOf.sizeOf(new Integer(1))); System.out.println(MySizeOf.sizeOf(new A())); System.out.println(MySizeOf.sizeOf(new B())); }
输出结果:
(指针压缩) 16 16 24 (指针未压缩)24 24 24
分析1(指针压缩):
new Integer(1)的大小=12B对象头+4B的实例数据+0B的填充=16Bnew A()的大小=12B对象头+4B的实例数据+0B的填充=16B new B()的大小=12B对象头+2*4B的实例数据=20B,填充之后=24B
分析2(指针未压缩):
new Integer(1)的大小=16B对象头+4B的实例数据+4B的填充=24B new A()的大小=16B对象头+4B的实例数据+4B的填充=24B new B()的大小=16B对象头+2*4B的实例数据+0B的填充=24B
案例3
System.out.println(MySizeOf.sizeOf(new int[2])); System.out.println(MySizeOf.sizeOf(new int[3])); System.out.println(MySizeOf.sizeOf(new char[2])); System.out.println(MySizeOf.sizeOf(new char[3]));
输出结果:
(指针压缩) 24 32 24 24 (指针未压缩) 32 40 32 32
分析1(指针压缩):
new int[2]的大小=12B对象头+压缩情况下数组比普通对象多4B来存放长度+2*4B的int实例大小=24B new int[3]的大小=12B对象头+4B长度+3*4B的int实例大小=28B,填充4B =32B new char[2]的大小=12B对象头+4B长度+2*2B的实例大小=20B,填充4B=24B new char[3]的大小=12B对象头+4B长度+3*2B的实例大小+2B填充=24B (PS:new char[5]的大小=32B)
分析2(指针未压缩):
new int[2]的大小=16B对象头+未压缩情况下数组比普通对象多8B来存放长度+2*4B实例大小=32B new int[3]的大小=16B+8B+3*4B+4B填充=40B new char[2]的大小=16B+8B+2*2B+4B填充=32B new char[2]的大小=16B+8B+3*2B+2B填充=32B (PS:new char[5]的大小为40B)
案例4(sizeOf只计算本体对象大小,fullSizeOf计算本体对象大小和引用的大小,具体可以翻阅附录1的代码).
System.out.println(MySizeOf.sizeOf(new String("a"))); System.out.println(MySizeOf.fullSizeOf(new String("a"))); System.out.println(MySizeOf.fullSizeOf(new String("aaaaa")));
输出结果:
(指针压缩)24 48 56 (指针未压缩)32 64 72
分析1(指针压缩):
翻看String(JDK7)的源码可以知道, String有这几个成员变量:(static变量属于类,不属于实例,所以声明为static的不计入对象的大小) private final char value[]; private int hash; private transient int hash32 = 0; MySizeOf.sizeOf(new String("a"))的大小=12B对象头+2*4B(成员变量hash和hash32)+4B(压缩的value指针)=24B MySizeOf.fullSizeOf(new String("a"))的大小=12B对象头+2*4B(成员变量hash和hash32)+4B指针+ (value数组的大小=12B对象头+4B数组长度+1*2B实例大小+6B填充=24B)=12B+8B+4B+24B=48B (PS: new String("aa"),new String("aaa"),new String("aaaa")的fullSizeOf大小都为48B) MySizeOf.fullSizeOf(new String("aaaaa"))的大小=12B+2*4B+4B+(12B+4B+5*2B+6B填充)=24B+32B=56B
分析2(指针未压缩)
MySizeOf.sizeOf(new String("a"))的大小=16B+2*4B+8B(位压缩的指针大小) =32B MySizeOf.fullSizeOf(new String("a"))的大小=16B对象头+2*4B(成员变量hash和hash32)+8B指针+(value数组的大小=16B对象头+8B数组长度+1*2B实例大小+6B填充=32B)=32B+32B=64B (PS: new String("aa"),new String("aaa"),new String("aaaa")的fullSizeOf大小都为64B) MySizeOf.fullSizeOf(new String("aaaaa"))的大小=16B+2*4B+8B+(16B+8B+5*2B+6B填充)=32B+40B=72B
这些计算结果只会少不会多,因为在代码运行过程中,一些对象的头部会伸展,mark区域会引用一些外部的空间(轻量级锁,偏向锁,这里不展开),所以官方给出的说明也是,最少会占用多少字节,绝对不会说只占用多少字节。
如果是32位的JDK,可以算一下或者运行一下上面各个案例的结果。
看来上面的这些我们来手动计算下new String()的大小:
1. 指针压缩的情况
12B对象头+2*4B实例变量+4B指针+(12B对象头+4B数组长度大小+0B实例大小)=24B+16B=40B
指针未压缩的情况
16B+2*4B+8B指针+(16B+8B数组长度大小+0B)=32B+24B=56B
所以一个空的String对象最少也要占用40B的大小,所以大家在以后应该编码过程中要稍微注意下。其实也并不要太在意,相信能从文章开头看到这里的同学敲的代码也数以万计了,不在意这些也并没有什么不妥之处,只不过如果如果你了解了的话对于提升自己的逼格以及代码优化水平有很大的帮助,比如:能用基本类型的最好别用其包装类。
附:agent.jar包源码
package com.zzh.size;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; public class MySizeOf{ 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<>(); 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物件大小淺析的詳細內容。更多資訊請關注PHP中文網其他相關文章!