如何精確地測量java對象的大小-底層instrument API

關於java對象的大小測量,網上有不少例子,大多數是申請一個對象後開始作GC,後對比先後的大小,不過這樣,雖說這樣測量對象的大小是可行的,不過未必是徹底準確的,由於過程當中包含對象自己的開銷,也許你運氣好,正好能碰上,差很少,不過這種測試每每顯得十分的笨重,由於要寫一堆代碼才能測試一點點東西,並且只能在本地測試玩玩,要真正測試實際的系統的對象大小這樣可就不行了,本文說說java一些比較偏底層的知識,如何測量對象大小,java其實也是有提供方法的。注意:本文的內容僅僅針對於Hotspot VM,若是你之前不知道jvm的對象大小怎麼測量,而又很想知道,跟我一步一步作一遍你就明白了。java

 

首先,咱們先寫一段你們可能不怎麼寫或者認爲不可能的代碼:一個類中,幾個類型都是private類型,沒有public方法,如何對這些屬性進行讀寫操做,看似不可能哦,爲何,這違背了面向對象的封裝,其實在必要的時候,留一道後面可使得語言的生產力更增強大,對象的序列化不會由於沒有public方法就沒法保存成功吧,OK,咱們簡單寫段代碼開個頭,逐步引入到怎麼樣去測試對象的大小,一下代碼很是簡單,相信不用我解釋什麼:node

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);,表明這個域的訪問被打開,比如是一道後門打開了,呵呵,上面的方法若是不設置這個,就直接報錯。oracle

看似和對象大小沒啥關係,不過這只是拋磚引玉,由於咱們首先要拿到對象的屬性,才能知道對象的大小,對象若是沒有提供public方法咱們也要知道它有哪些屬性,因此咱們後面多半會用到這段相似的代碼哦!jvm

對象測量大小的方法關鍵爲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:編寫測試類,測試類中寫:spa

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參數設置不同,對象大小也不同大)。

一、首先看Integer對象,在32bit模式下,_class區域佔用4byte,_mark區域佔用最少4byte,因此最少8byte頭部,Integer內部有一個int類型的數據,佔4個byte,因此此時爲8+4=12,java默認要求按照8byte對象對其,因此對其到16byte,因此咱們理論結果第一個應該是16;

二、再看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;

三、第三個在第二個基礎上已經分析,就是16byte大小

也就是理論結果是:1六、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,經過上面已經能夠看出,保持了原有方法,由於深度遞歸畢竟比較慢,咱們有些時候能夠選擇到底用那一種:

回到步驟從新作一次:

一、編譯agent

二、打包class,並修改META-INF/MANIFEST.MF文件中增長一行:Premain-Class: MySizeOf

三、修改測試類:
 

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]));  
   }   
}

 

四、設置環境變量開始運行(若是已經設置好了就無需重複設置):

D:\>javac TestSize.java


D:\>java -javaagent:agent.jar TestSize
16
24
40
16

這個結果是咱們想要的了,看來這個測試是靠譜的,面對理論和測試結果,以及上面所謂的對齊方法,你們能夠本身編寫一些類的對象來測試大小看時候和實際的保持一致;

 

最後,文章補充一些:

一、對象採用8字節對齊的方式是不論32bit仍是64bit都是同樣的

二、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並不看重性能和空間,不過若是你的代碼寫得好一樣會跑得更加快。

相關文章
相關標籤/搜索