深入Java對象大小

 

在大規模Java 應用開發中,總會遇到內存泄漏的問題。通常的做法,通過 Profile 工具,分析 Java  Heap ,一般能夠發現哪些對象內存佔用巨大,而引起的泄漏問題。爲了更好地深入瞭解問題的本質,以及從另外一個角度來分析問題,特寫這篇文章。

 

可能不少的讀者,並不清楚Java 對象到底佔居多少的空間(單位:字節 =8 比特)。文章中會使用 JDK 6 update 7 自帶的 Profile 工具 -Java VisualVM 。引入 Profile 工具的目的正是爲了分析對象的大小。

 

首先,要區別Java 對象和 Java 類元信息,其中, JVM 把所有的 Java 對象放到 Java Heap 中,而類的元信息是放在方法區的,通俗地說,在 Java 源代碼中,定義的字段和方法等變量標示(比如字段名稱和類型等)。請注意,元信息所引用的對象還是在 Java Heap 裏面。那麼,本章主要 針對 的是Java Heap

 

       早幾天,看到了JavaEye 上面提問 -http://www.iteye.com/problems/45423 。問題的本質,和主題一樣,不過它想要通過 Java 程序來計算,貌似有點困難。以前外國一個哥們( http://www.javaworld.com/javaworld/javatips/jw-javatip130.html )也寫 一個程序計算對象大小,它的計算如下:

public class Sizeof

{

    public static void main (String [] args) throws Exception

    {

        // Warm up all classes/methods we will use

        runGC ();

        usedMemory ();

        // Array to keep strong references to allocated objects

        final int count = 100000;

        Object [] objects = new Object [count];        

        long heap1 = 0;

        // Allocate count+1 objects, discard the first one

        for (int i = -1; i < count; ++ i)

        {

            Object object = null;

            // Instantiate your data here and assign it to object

             object = new Object ();

            if (i >= 0)

                objects [i] = object;

            else

            {

                object = null; // Discard the warm up object

                runGC ();

                heap1 = usedMemory (); // Take a before heap snapshot

            }

        }

        runGC ();

        long heap2 = usedMemory (); // Take an after heap snapshot:        

        final int size = Math.round (((float)(heap2 - heap1))/count);

        System.out.println ("'before' heap: " + heap1 +

                            ", 'after' heap: " + heap2);

        System.out.println ("heap delta: " + (heap2 - heap1) +

            ", {" + objects [0].getClass () + "} size = " + size + " bytes");

        for (int i = 0; i < count; ++ i) objects [i] = null;

        objects = null;

    }

    private static void runGC () throws Exception

    {

        // It helps to call Runtime.gc()

        // using several method calls:

        for (int r = 0; r < 4; ++ r) _runGC ();

    }

    private static void _runGC () throws Exception

    {

        long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE;

        for (int i = 0; (usedMem1 < usedMem2) && (i < 500); ++ i)

        {

            s_runtime.runFinalization ();

            s_runtime.gc ();

            Thread.currentThread ().yield ();

            

            usedMem2 = usedMem1;

            usedMem1 = usedMemory ();

        }

    }

    private static long usedMemory ()

    {

        return s_runtime.totalMemory () - s_runtime.freeMemory ();

    }

    

    private static final Runtime s_runtime = Runtime.getRuntime ();

} // End of class

( 代碼 1)

 

通過改變紅色區域,來切換測試對象。先運行出結果,以下結果是在Windows XP x86  ,SUN JDK 1.6.0 update 7 ,並且Console 信息部分被截斷:

 

{class java.lang.Object} size = 8 bytes

{class java.lang.Integer} size = 16 bytes

{class java.lang.Long} size = 16 bytes

{class java.lang.Byte} size = 16 bytes

{class [Ljava.lang.Object;} size = 16 bytes //長度爲0的Object類型數組

{class [Ljava.lang.Object;} size = 16 bytes //長度爲1的Object類型數組

{class [Ljava.lang.Object;} size = 24 bytes //長度爲2的Object類型數組

 

現在,這個結論有問題,因爲,從Byte、Long、Integer源代碼的角度,不可能對象空間大小相同,那麼接下來需要藉助於Java VisualVM來做了。 在測試之前 ,需要一段輔助程序,幫助執行。實現如下:

 

public   class  MainClass {

/**

 * 啓動方法

 *  @param  args

 */

public   static   void  main(String[] args)  throws  Exception {

Object object =  new  Object();

neverStop ();

neverGC (object);

}

private   static   void  neverGC(Object object) {

System. out .println(object);

}

private   static   void  neverStop()  throws  InterruptedException {

Thread. sleep (Long. MAX_VALUE );

}

}

( 代碼2 )

這個程序保證對象不會被GC掉,並且不會停止。

首先,在Java VisualVM上面,選擇正確的程序進行監控,然後做一個Heap Dump。



 (圖1)

Dump之後,點擊 Classes ,然後過濾出 java.lang.Object ,如圖所示:



 (圖2)


 (圖3)

 

 

3 過濾出三個結果,暫時不看數組對象,選擇java.lang.Object ,然後點擊 instances 按鈕,或者雙擊 java.lang.Object


 

( 4)

結果發現,Object 對象的大小總是 8 個字節。再看看 Integer Long Byte Sizeof 類的計算結果不可信,分析錯誤原因放一下,後面會提到。先來分析 Integer Long Byte 。從源代碼的角度來分析, Integer 包含了一個 int value, 其他的也有對應類型。而知曉, int 佔用 4 個字節, long 則是 8 個字節, byte 佔用 1 個字節。看圖說話:


 

(圖5)

 

 (圖6)


(圖7)

 

 

Integer對象空間大小是 12 字節(見圖 5) Long 對象空間則是 16 個字節(見圖 6 ), Byte 空間則是 9 個字節(見圖 7 )。那麼除去對象自身的狀態 value ,得出的結論是,最後“空殼”對象都 8 字節?不能確定,因爲還有類(靜態)字段沒有考慮,這些字段是否屬於對象實例的一部分呢?很多書上面再三強調類的字段不屬於對象實例,對於這種說法需要保持懷疑的態度?下面做一個“空殼”對象實驗,代碼如下:

 

/**

 * 和Object一樣,沒有任何添加對象狀態。

 *  @author  mercy

 */

public   class  SameAsObject  extends  Object {

}

( 代碼3 )

修改 MainClass程序, new  一個 SameAsObject 對象,重新 Heap Dump,結果圖:

 


 

 

 (圖8)

 

8 中表明,證明了“空殼”對象空間大小 8 字節,和 java.lang.Object 類似。目前還不能證明 Integer 的情況,因爲 Integer 類的層次是 Integer <- Number <- Object 。雖然 Number 沒有對象狀態,可是也不能證明出去 value Integer 對象空間大小等於 Object 的。爲了證明這一點,再擴展一下 SameAsObject, 使其成爲父類,創建一個子類 ExtSameAsObject 。同樣的方法,獲取大小:

 

 

 (圖9)

 

9 中, ExtSameAsObject 的空間大小還是 8 字。 證明了空殼對象大小等同於java.lang.Object 的。 那麼自然地可以推導出java.lang.Double 也是 16 字節( 8 字節空殼對象 +8 字節的 double 類型 value)

 

細心的讀者會發現,圖8 9 中,筆者標記了紅色區域,都有 Java frame 的標識。而在MainClass main 方法中,有一句: Object object =  new  ExtSameAsObject(); 這個對象是局部變量。說明什麼問題呢? 兩個問題:第一,正因爲在方法內部執行,這個語句就是一個 frame, frame Java Stack 的組成單位。這個 frame 被壓入棧,並且類型是 ExtSameAsObject ,同時帶有一個對象的地址 #1 (當然這裏不是實際地址,只是一個引用標識)。第二,即使是局部對象,對象仍然分配在 Java Heap 中。

 

既然“空殼”的Integer 對象,佔用 8 個字節的大小,那麼類的成員就不應該歸入對象實例之中。從實驗中,我們可以得出結論, 某個類的任何類成員的常量和變量,都不會分配(或計算)到該類的對象實例。

 

回到Sizeof 類,爲什麼 Sizeof 會計算出問題?雖然 Sizeof 類計算有誤,不過它的思想還是幾點值得借鑑:

 

第一、 作者深入了了解了Runtime#gc() 方法和 Runtime#runFinalization 方法的語義。這兩個方法並不是實時執行,而是建議 JVM 執行,執行與否程序怎麼知道呢 ? 在— _runGC 方法中,作者試圖通過內存的變化來判斷是否 GC GC 後,肯定會變化的),確實有道理。

 

第二、 通過N 次,求得平均數,比單次測試要精確很多。

 

第三、 沒有開闢其他對象,不影響結果。

 

同時,不過作者忽略瞭如下情況:

 

第一、 GC不只是針對需要測試的對象,而是整個 JVM 。因此在 GC 的時候,有可能是 JVM 啓動後,把其他沒有使用的對象給 GC 了,這樣造成了使用空間的變大,而 Heap 空間變小。

 

第二、 Runtime#freeMemory()方法,返回的數據是 近視值 ,不一定能夠保證正確性。因此在後面的累計、求平均數來帶了誤差。

 

第三、 使用了Math#round() 方法,又帶來了誤差。

 

由於Sizeof 的不精確,不能作爲測試基準。

 

那麼,更多的疑問產生了。前面測試的都是單一對象,那麼數組對象是如何分配的?

 

修改程序如下:

 

public   static   void  main(String[] args)  throws  Exception {

//Object object = new ExtSameAsObject();

Object [] object =  new   Object [0];

...

( 代碼4 )

 

重新Heap Dump:


 (圖10)

 

餘下內容,看附件。錯誤內容已經修正,請下載Fix1文檔

 

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