如何精確地測量java對象的大小-底層instrument API
關於java對象的大小測量,網上有很多例子,大多數是申請一個對象後開始做GC,後對比前後的大小,不過這樣,雖然說這樣測量對象的大小是可行的,不過未必是完全準確的,因為過程中包含對象本身的開銷,也許你運氣好,正好能碰上,差不多,不過這種測試往往顯得十分的笨重,因為要寫一堆代碼才能測試一點點東西,而且隻能在本地測試玩玩,要真正測試實際的係統的對象大小這樣可就不行了,本文說說java一些比較偏底層的知識,如何測量對象大小,java其實也是有提供方法的。注意:本文的內容僅僅針對於Hotspot VM,如果你以前不知道jvm的對象大小怎麼測量,而又很想知道,跟我一步一步做一遍你就明白了。
首先,我們先寫一段大家可能不怎麼寫或者認為不可能的代碼:一個類中,幾個類型都是private類型,沒有public方法,如何對這些屬性進行讀寫操作,看似不可能哦,為什麼,這違背了麵向對象的封裝,其實在必要的時候,留一道後麵可以使得語言的生產力更加強大,對象的序列化不會因為沒有public方法就無法保存成功吧,OK,我們簡單寫段代碼開個頭,逐步引入到怎麼樣去測試對象的大小,一下代碼非常簡單,相信不用我解釋什麼:
import java.lang.reflect.Field; class NodeTest1 { private int a = 13; private int b = 21; } public class Test001 { public static void main(String []args) { NodeTest1 node = new NodeTest1(); Field []fields = NodeTest1.class.getDeclaredFields(); for(Field field : fields) { field.setAccessible(true); try { int i = field.getInt(node); field.setInt(node, i * 2); System.out.println(field.getInt(node)); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } }
代碼最基本的意思就是:實例化一個NodeTest1這個類的實例,然後取出兩個屬性,分別乘以2,然後再輸出,相信大家會認為這怎麼可能,NodeTest1根本沒有public方法,代碼就在這裏,將代碼拷貝回去運行下就OK了,OK,現在不說這些了,運行結果為:
26
42
為什麼可以取到,是每個屬性都留了一道門,主要是為了自己或者外部接入的方便,相信看代碼自己仔細的朋友,應該知道門就在:field.setAccessible(true);,代表這個域的訪問被打開,好比是一道後門打開了,嗬嗬,上麵的方法如果不設置這個,就直接報錯。
看似和對象大小沒啥關係,不過這隻是拋磚引玉,因為我們首先要拿到對象的屬性,才能知道對象的大小,對象如果沒有提供public方法我們也要知道它有哪些屬性,所以我們後麵多半會用到這段類似的代碼哦!
對象測量大小的方法關鍵為java提供的(1.5過後才有):java.lang.instrument.Instrumentation,它提供了豐富的對結構的等各方麵的跟蹤和對象大小的測量的API(本文隻闡述對象大小的測量方法),於是乎我心喜了,不過比較惡心的是它是實例化類:sun.instrument.IntrumentationImpl是sun開頭的,這個鬼東西有點不好搞,翻開源碼構造方法是private類型,沒有任何getInstance的方法,寫這個類幹嘛?看來這個隻能被JVM自己給初始化了,那麼怎麼將它自己初始化的東西取出來用呢,唯一能想到的就是agent代理,那麼我們先拋開代理,首先來寫一個簡單的對象測量方法:
//步驟1(先創建一個用於測試對象大小的處理類):
import java.lang.instrument.Instrumentation; public class MySizeOf { private static Instrumentation inst; /** *這個方法必須寫,在agent調用時會被啟用 */ public static void premain(String agentArgs, Instrumentation instP) { inst = instP; } //用來測量java對象的大小(這裏先理解這個大小是正確的,後麵再深化) public static long sizeOf(Object o) { if(inst == 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 inst.getObjectSize(o); } }
//步驟2:上麵我們寫好了agent的代碼,此時我們要將上麵這個類編譯後打包為一個jar文件,並且在其包內部的META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf代表執行代理的全名,這裏的類名稱是沒有package的,如果你有package,那麼就寫全名,我們這裏假設打包完的jar包名稱為agent.jar(打包過程這裏簡單闡述,就不細說了),OK,繼續向下走:
//步驟3:編寫測試類,測試類中寫:
public class TestSize { public static void main(String []args) { System.out.println(MySizeOf.sizeOf(new Integer(1))); System.out.println(MySizeOf.sizeOf(new String("a"))); System.out.println(MySizeOf.sizeOf(new char[1])); } }
下一步準備運行,運行前我們準備初步估算下結果是什麼,目前我是在32bit模式下運行jvm(注意,不同位數的JVM參數設置不一樣,對象大小也不一樣大)。
1、首先看Integer對象,在32bit模式下,_class區域占用4byte,_mark區域占用最少4byte,所以最少8byte頭部,Integer內部有一個int類型的數據,占4個byte,所以此時為8+4=12,java默認要求按照8byte對象對其,所以對其到16byte,所以我們理論結果第一個應該是16;
2、再看String,長度為1,String對象內部本身有4個非靜態屬性(靜態屬性我們不計算空間,因為所有對象都是共享一塊空間的),4個非靜態屬性中,有offset、count、hash為int類型,分別占用4個byte,char value[]為一個指針,指針的大小在bit模式下或64bit開啟指針壓縮下默認為4byte,所以屬性占用了16byte,String本身有8直接頭部,所以占用了24byte;其次,一個String包含了子對象char數組,數組對象和普通對象的區別是需要用一個字段來保存數組的長度,所以頭部變成12字節,java中一個char采用UTF-16編碼,占用2個byte,所以是14byte,對其到16byte,24+16=40byte;
3、第三個在第二個基礎上已經分析,就是16byte大小
也就是理論結果是:16、40、16;
//步驟3:現在開始運行代碼:
運行代碼前需要保證classpath把剛才的agent.jar包含進去:
D:\>javac TestSize.java
D:\>java -javaagent:agent.jar TestSize
16
24
16
第一個和第三個結果一致了,不過奇怪了,第二個怎麼是24,不是40,怎麼和理論結果偏差這麼大,再回到理論結果中,有一個24曾經出現過,24是指String而不包含char數組的空間大小,那麼這麼算還真是對的,可見,java默認提供的方法隻能測量對象當前的大小,如果要測量這個對象實際的大小(也就是包含了子對象,那麼就需要自己寫算法來計算了,最簡單的方法就是遞歸,不過遞歸一項是我不喜歡用的,無意中在一個地方看到有人用棧寫了一個代碼寫得還不錯,自己稍微改了下,就是下麵這種了)。
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; public class MySizeOf { static Instrumentation inst; public static void premain(String agentArgs, Instrumentation instP) { inst = instP; } public static long sizeOf(Object o) { if(inst == 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 inst.getObjectSize(o); } public static long fullSizeOf(Object obj) {//深入檢索對象,並計算大小 Map<Object, Object> visited = new IdentityHashMap<Object, Object>(); Stack<Object> stack = new Stack<Object>(); 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<Object, Object> visited) { if (obj instanceof String) { if (obj == ((String) obj).intern()) { return true; } } return (obj == null) || visited.containsKey(obj); } private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) { if (skipObject(obj, visited)) {//跳過常量池對象、跳過已經訪問過的對象 return 0; } visited.put(obj, null);//將當前對象放入棧中 long result = 0; result += sizeOf(obj); Class <?>clazz = obj.getClass(); if (clazz.isArray()) {//如果數組 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; } return getNodeSize(clazz , result , obj , stack); } //這個方法獲取非數組對象自身的大小,並且可以向父類進行向上搜索 private static long getNodeSize(Class <?>clazz , long result , Object obj , Stack<Object> stack) { while (clazz != null) { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (!Modifier.isStatic(field.getModifiers())) {//這裏拋開靜態屬性 if (field.getType().isPrimitive()) {//這裏拋開基本關鍵字(因為基本關鍵字在調用java默認提供的方法就已經計算過了) continue; }else { field.setAccessible(true); try { Object objectToAdd = field.get(obj); if (objectToAdd != null) { stack.add(objectToAdd);//將對象放入棧中,一遍彈出後繼續檢索 } } catch (IllegalAccessException ex) { assert false; } } } } clazz = clazz.getSuperclass();//找父類class,直到沒有父類 } return result; } }
OK,通過上麵已經可以看出,保持了原有方法,因為深度遞歸畢竟比較慢,我們有些時候可以選擇到底用那一種:
回到步驟重新做一次:
1、編譯agent
2、打包class,並修改META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf
3、修改測試類:
public class TestSize { public static void main(String []args) { System.out.println(MySizeOf.sizeOf(new Integer(1))); System.out.println(MySizeOf.sizeOf(new String("a"))); System.out.println(MySizeOf.fullSizeOf(new String("a"))); System.out.println(MySizeOf.sizeOf(new char[1])); } }
4、設置環境變量開始運行(如果已經設置好了就無需重複設置):
D:\>javac TestSize.java
D:\>java -javaagent:agent.jar TestSize
16
24
40
16
這個結果是我們想要的了,看來這個測試是靠譜的,麵對理論和測試結果,以及上麵所謂的對齊方法,大家可以自己編寫一些類的對象來測試大小看時候和實際的保持一致;
最後,文章補充一些:
1、對象采用8字節對齊的方式是不論32bit還是64bit都是一樣的
2、java在64bit模式下開啟指針壓縮,比32bit模式下,頭部會大4byte(_mark區域變成8byte,_class區域被壓縮),如果沒有開啟指針壓縮,頭部會大8byte(_mark和_class都會變成8byte),jdk1.6推出參數-XX:+UseCompressedOops,在32G內存一下默認會自動打開這個參數,如下:
[xieyu@oracle001 ~]$ java -Xmx31g -XX:+PrintFlagsFinal |grep Compress bool SpecialStringCompress = true {product} bool UseCompressedOops := true {lp64_product} bool UseCompressedStrings = false {product} [xieyu@oracle001 ~]$ java -Xmx32g -XX:+PrintFlagsFinal |grep Compress bool SpecialStringCompress = true {product} bool UseCompressedOops = false {lp64_product} bool UseCompressedStrings = false {product}
簡單計算一個,在指針壓縮的情況下,一個new String("a");這個對象的空間大小為:12字節頭部+4*4 = 28字節對齊到32字節,然後c所指向的char數組頭部比普通對象多4個byte來存放長度,12+4+2byte的字符=16,也就是48個byte,其實即使你new String()也會占這麼大的空間,因為有對齊,如果字符的長度是8個,那麼就是12+4+16=32,也就是有64byte;
如果不開啟指針壓縮再算算:頭部變成16byte + 4*3個int數據 + 8(1個指針) = 36對齊到40byte,對應的char數組的頭部變成16+4 + 2 = 22對齊到24byte,40+24=64,也就是隻有一個字符或者0個字符都會對齊到64byte,所以,你懂的,參數該怎麼調,代碼該怎麼寫,如果長度為8個字符的那麼後麵部分就會變成16+4+16=36對齊到40byte,40+40=80byte,也就是說,拋開其他的引用空間(比如通過數組或集合類引用),如果你有10來個String,每個大小就裝8個字符,就會有1K的大小,你的代碼裏頭有多少?嗬嗬!
這些不是我說的,這些是一種計算方法,而且這個計算結果隻會少不會多,因為代碼運行過程中,一些對象的頭部會伸展,_mark區域裝不下會用外部的空間來存放,所以官方給出的說明也是,最少會占用多少字節,絕對不會說隻占用多少字節。
OK,說得挺嚇人的,不過寫代碼還是不要怕,不過就這些而言,隻是說明java是如何浪費空間的,不要一味使用一些高級的東西,在必要的時候,考慮性能還是有很大的空間,類似集合類以及多維數組,前麵的引用其實和數據一點關係都沒有,但是占用的空間比數據本身都要大很多。
本文隻是通過一種方式讓大家知道如何去測量對象大小,同時知道一個java對象如何開銷內存,開銷而且很大,所以回過頭來說,即使java並不看重性能和空間,不過如果你的代碼寫得好同樣會跑得更加快。
最後更新:2017-04-02 06:52:15