如何精確地測量java對象的大小

關於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本身有8byte頭部,所以佔用了24byte;其次,一個String包含了子對象char數組,數組對象和普通對象的區別是需要用一個字段來保存數組的長度,所以頭部變成12byte,java中一個char採用UTF-16編碼,佔用2個byte,所以是14byte,對其到16byte,24+16=40byte; 
(3) 第三個在第二個基礎上已經分析,就是16byte大小;

也就是理論結果是:16、40、16;

步驟4:現在開始運行代碼:運行代碼前需要保證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;  
  }  
}

修改測試類:

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

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

最後,文章補充一些:

  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是如何浪費空間的,不要一味使用一些高級的東西,在必要的時候,考慮性能還是有很大的空間,類似集合類以及多維數組,前面的引用其實和數據一點關係都沒有,但是佔用的空間比數據本身都要大很多。




在eclipse中如何運行:

先創建件MySizeof的maven項目,然後使用jar插件打jar包,不過jar插件要做一些額外的配置:

<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jar-plugin</artifactId>
				<version>2.5</version>
				<configuration>
					<archive>
						<manifestEntries>
							<!-- 指定代理類 -->
							<Premain-Class>com.component.my_sizeof.MySizeOf</Premain-Class>
							<!-- 自定義值 -->
							<author>張三</author>
						</manifestEntries>
						<manifest>
							<addClasspath>true</addClasspath>
						</manifest>
					</archive>
				</configuration>
			</plugin>

運行jar:jar命令,生成jar包

新建一個普通maven java項目,編寫測試類(必須要main方法的那種)

package web_acl;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import web_acl.vo.UserVO;

import com.component.my_sizeof.MySizeOf;

public class MainTest {

	public static void main(String[] args) {
		Map<String, Object> map = new HashMap<String, Object>();
		System.out.println(MySizeOf.fullSizeOf(map));
		map.put("key", "abc");
		System.out.println(MySizeOf.fullSizeOf(map));
		System.out.println("###############################");
		Map<String, Object> mapTmp = new HashMap<String, Object>(1);
		System.out.println(MySizeOf.fullSizeOf(mapTmp));
		mapTmp.put("key", "abc");
		mapTmp.put("adt", "abc");
		System.out.println(MySizeOf.fullSizeOf(mapTmp));
		System.out.println("###############################");
		UserVO user = new UserVO();
		System.out.println(MySizeOf.fullSizeOf(user));
		user.setCreatorId(1);
		user.setEditDate(new Date());
		user.setPwd("abc");
		System.out.println(MySizeOf.fullSizeOf(user));
	}
	
}

配置運行參數,然後運行








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章