1. 程式人生 > >Ehcache計算Java物件記憶體大小

Ehcache計算Java物件記憶體大小

在EHCache中,可以設定maxBytesLocalHeap、maxBytesLocalOffHeap、maxBytesLocalDisk值,以控制Cache佔用的記憶體、磁碟的大小(注:這裡Off Heap是指Element中的值已被序列化,但是還沒寫入磁碟的狀態,貌似只有企業版的EHCache支援這種配置;而這裡maxBytesLocalDisk是指在最大在磁碟中的資料大小,而不是磁碟檔案大小,因為磁碟文中有一些資料是空閒區),因而EHCache需要有一種機制計算一個類在記憶體、磁碟中佔用的位元組數,其中在磁碟中佔用的位元組大小計算比較容易,只需要知道序列化後位元組陣列的大小,並且加上一些統計資訊,如過期時間、磁碟位置、命中次數等資訊即可,而要計算一個物件例項在記憶體中佔用的大小則要複雜一些。

計算一個例項記憶體佔用大小思路
在Java中,除了基本型別,其他所有通過欄位包含其他例項的關係都是引用關係,因而我們不能直接計算該例項佔用的記憶體大小,而是要遞迴的計算其所有欄位佔用的記憶體大小的和。在Java中,我們可以將所有這些通過欄位引用簡單的看成一種樹狀結構,這樣就可以遍歷這棵樹,計算每個節點佔用的記憶體大小,所有這些節點佔用的記憶體大小的總和就當前例項佔用的記憶體大小,遍歷的演算法有:先序遍歷、中序遍歷、後序遍歷、層級遍歷等。但是在實際情況中很容易出現環狀引用(最簡單的是兩個例項之間的直接引用,還有是多個例項構成的一個引用圈),而破壞這種樹狀結構,而讓引用變成圖狀結構。然而圖的遍歷相對比較複雜(至少對我來說),因而我更願意把它繼續看成一顆樹狀圖,採用層級遍歷,通過一個IdentitySet紀錄已經計算過的節點(例項),並且使用一個Queue來紀錄剩餘需要計算的節點。演算法步驟如下:
1. 先將當前例項加入Queue尾中。
2. 迴圈取出Queue中的頭節點,計算它佔用的記憶體大小,加到總記憶體大小中,並將該節點新增到IdentitySet中。
3. 找到該節點所有非基本型別的子節點,對每個子節點,如果在IdentityMap中沒有這個子節點的例項,則將該例項加入的Queue尾。
4. 回到2繼續計算直到Queue為空。
剩下的問題就是如何計算一個例項本身佔用的記憶體大小了。這個以我目前的經驗,我只能想到遍歷一個例項的所有例項欄位,根據每個欄位的型別來判斷每個欄位佔用的記憶體大小,然後它們的和就是該例項佔用的總記憶體的大小。對於欄位的型別,首先是基本型別欄位,byte、boolean佔一個位元組,short、char佔2個位元組,int、float佔4個位元組,double佔8個位元組等;然後是引用型別,對型別,印象中虛擬機器規範中沒有定義其大小,但是一般來說對32位系統佔4個位元組,對64位系統佔8個位元組;再就是對陣列,基本型別的陣列,byte每個元素佔1個位元組,short、char每個元素佔2個位元組,int每個元素佔4個位元組,double每個元素佔8個位元組,引用型別的陣列,先計算每個引用元素佔用的位元組數,然後是引用本省佔用的位元組數。
以上是我對EHCache中計算一個例項邏輯不瞭解的時候的個人看法,那麼接下來我們看看EHCache怎麼來計算。

Java物件記憶體結構(以Sun JVM為例)
參考:http://www.importnew.com/1305.html,之所以把參考連結放在開頭是因為下面基本上是對連結所在文章的整理,之所以要整理一遍,一是怕原連結文章消失,二則是為了加深自己的理解。
在Sun JVM中,除陣列以外的物件都有8個位元組的頭部(陣列還有額外的4個位元組頭部用於存放長度資訊),前面4個位元組包含這個物件的標識雜湊碼以及其他一些flag,如鎖狀態、年齡等標識資訊,後4個位元組包含一個指向物件的類例項(Class例項)的引用。在這頭部8個位元組之後的記憶體結構遵循一下5個規則:
規則1: 任何物件都是以8個位元組為粒度進行對齊的。
比如對一個Object類,因為它沒有任何例項,因而它只有8個頭部直接,則它佔8個位元組大小。而對一個只包含一個byte欄位的例項,它需要填上(padding)7個位元組的大小,因而它佔16個位元組,典型的如一個Boolean例項要佔用16個位元組的記憶體!

class MyClass {
    byte a;
}
[HEADER:    8 bytes] 8
[a:             1 byte ] 9
[padding:    7 bytes] 16

規則2: 類屬性按照如下優先順序進行排列:長整型和雙精度型別;整型和浮點型;字元和短整型;位元組型別和布林型別;最後是引用型別。這些屬性都按照各自的單位對齊。
在Java物件記憶體結構中,物件以上述的8個位元組的頭部開始,然後物件屬性緊隨其後。為了節省記憶體,Sun VM並沒有按照屬性宣告時順序來進行記憶體佈局,而是使用如下順序排列:
1. 雙精度型(double)和長整型(long),8位元組。
2. 整型(int)和浮點型(float),4位元組。
3. 短整型(short)和字元型(char),2位元組。
4. 布林型(boolean)和位元組型(byte),2位元組。
5. 引用型別。
並且物件屬性總是以它們的單位對齊,對於不滿4位元組的資料型別,會填充未滿4位元組的部分。之所以要填充是出於效能考慮:因為從記憶體中讀取4位元組資料到4位元組暫存器的動作,如果資料以4位元組對齊的情況小,效率要高的多。

class MyClass {
    byte a;
    int c;
    boolean d;
    long e;
    Object f;
}
//如果JVM不對其重排序,它要佔40個位元組
[HEADER:    8 bytes] 8
[a:             1 byte ] 9
[padding:    3 bytes] 12
[c:             4 bytes] 16
[d:             1 byte ] 17
[padding:    7 bytes] 24
[e:             8 bytes] 32
[f:              4 bytes] 36
[padding:     4 bytes] 40
//經JVM重排序後,只需要佔32個位元組
[HEADER:       8 bytes] 8
[e:                8 bytes] 16
[c:                4 bytes] 20
[a:                1 byte ] 21
[d:                1 byte ] 22
[padding:       2 bytes] 24
[f:                4 bytes] 28
[padding:       4 bytes] 32

規則3: 不同類繼承關係中的成員不能混合排列。首先按照規則2處理父類中的成員,接著才是子類的成員。

class A {
    long a;
    int b;
    int c;
}
class B extends A {
    long d;
}
[HEADER:      8 bytes] 8
[a:               8 bytes] 16
[b:               4 bytes] 20
[c:               8 bytes] 32

規則4: 當父類最後一個屬性和子類第一個屬性之間間隔不足4位元組時,必須擴充套件到4個位元組的基本單位。

class A {
    byte a;
}
class B extends A {
    byte b;
}
[HEADER:    8 bytes] 8
[a:             1 byte ] 9
[padding:    3 bytes] 12
[b:             1 byte ] 13
[padding:    3 bytes] 16

規則5: 如果子類第一個成員時一個雙精度或長整型,並且父類沒有用完8個位元組,JVM會破壞規則2,按整型(int)、短整型(short)、位元組型(byte)、引用型別(reference)的順序向未填滿的空間填充。

class A {
    byte a;
}
class B extends A {
    long b;
    short c;
    byte d;
}
[HEADER:    8 bytes] 8
[a:             1 byte ] 9
[padding:    3 bytes] 12
[c:             2 bytes] 14
[d:             1 byte ] 15
[padding:    8 bytes] 24

陣列記憶體佈局
陣列物件除了作為物件而存在的頭以外,還存在一個額外的頭部成員用來存放陣列的長度,它佔4個位元組。

//三個元素的位元組陣列
[HEADER:    12 bytes] 12
[[0]:             1  byte ] 13
[[1]:              1 byte ] 14
[[2]:              1 byte ] 15
[padding:      1 byte ] 16
//三個元素的長整型陣列
[HEADER:     12 bytes] 12
[padding:     4 bytes ] 16
[[0]:               8 bytes] 24
[[1]:               8 bytes] 32
[[2]:               8 bytes] 40

非靜態內部類
非靜態內不累它又一個額外的“隱藏”成員,這個成員時一個指向外部類的引用變數。這個成員是一個普通引用,因此遵循引用記憶體佈局的規則。因此內部類有4個位元組的額外開銷。

EHCache計算一個例項佔用的記憶體大小
EHCache中計算一個例項佔用記憶體大小的基本思路和以上類似:遍歷例項數上的所有節點,對每個節點計算其佔用的記憶體大小。不過它結構設計的更好,而且它有三種用於計算一個例項佔用記憶體大小的實現。我們先來看這三種用於計算一個例項佔用記憶體大小的邏輯:

  1. ReflectionSizeOf
    使用反射的方式計算計算一個例項佔用的記憶體大小就是我上面想到的這種方法。

    因為使用反射計算一個例項佔用記憶體大小的根據不同虛擬機器的特性是來判斷一個例項的各個欄位佔用的大小以及該例項儲存額外資訊佔用的大小,因而EHCache中採用JvmInformation列舉型別來抽象這種對不同虛擬機器實現的不同:
    JVM Desc PointerSize JavaPointerSize MinimumObjectSize ObjectAlignment ObjectHeaderSize FieldOffsetAdjustment AgentSizeOfAdjustment
    HotSpot 32-Bit 4 4 8 8 8 0 0
    HotSpot 32-Bit with Concurrent Mark-and-Sweep GC 4 4 16 8 8 0 0
    HotSpot 64-Bit 8 8 8 8 16 0 0
    HotSpot 64-Bit With Concurrent Mark-and-Sweep GC 8 8 24 8 16 0 0
    HotSpot 64-Bit with Compressed OOPs 8 4 8 8 12 0 0
    HotSpot 64-Bit with Compressed OOPs and Concurrent Mark-and-Sweep GC 8 4 24 8 12 0 0
    JRockit 32-Bit 4 4 8 8 16 8 8
    JRockit 64-Bit(with no reference compression) 4 4 8 8 16 8 8
    JRockit 64-Bit with 4GB compressed References 4 4 8 8 16 8 8
    JRockit 64-Bit with 32GB Compressed References 4 4 8 8 16 8 8
    JRockit 64-Bit with 64GB Compressed References 4 4 16 16 24 16 16
    IBM 64-Bit with Compressed References 4 4 8 8 16 0 0
    IBM 64-Bit with no reference compression 8 8 8 8 24 0 0
    IBM 32-Bit 4 4 8 8 16 0 0
    UNKNOWN 32-Bit 4 4 8 8 8 0 0
    UNKNOWN 64-Bit 8 8 8 8 16 0 0

    ObjectAligment default: 8
    MinimumObjectSize default equals ObjectAligment
    ObjectHeaderSize default: PointerSize + JavaPointerSize
    FIeldOffsetAdjustment default: 0
    AgentSizeOfAdjustment default: 0
    ReferenceSize equals JavaPointerSize
    ArrayHeaderSize: ObjectHeaderSize + 4(INT Size)
    JRockit and IBM JVM do not support ReflectionSizeOf


    而對基本型別,則因為虛擬機器的規範,它們都是相同的,EHCache中採用PrimitiveType列舉型別來定義不同基本型別的長度:
    enum PrimitiveType {
        BOOLEAN(boolean.class, 1),
        BYTE(byte.class, 1),
        CHAR(char.class, 2),
        SHORT(short.class, 2),
        INT(int.class, 4),
        FLOAT(float.class, 4),
        DOUBLE(double.class, 8),
        LONG(long.class, 8);

        private Class<?> type;
        private int size;

        public static int getReferenceSize() {
            return CURRENT_JVM_INFORMATION.getJavaPointerSize();
        }
        public static long getArraySize() {
            return CURRENT_JVM_INFORMATION.getObjectHeaderSize() + INT.getSize();
        }
    }
    反射計算一個例項(instance)佔用記憶體大小(size)步驟如下:
    a. 如果instance為null,size為0,直接返回。
    b. 如果instance是陣列型別,size為陣列頭部大小+每個陣列元素佔用大小*陣列長度+填充到物件對齊最小單位,最後保證如果size要比物件最小大小大過相等。
    c. 如果instance是普通例項,size初始值為物件頭部大小,然後找到物件對應類的所有繼承類,從最頂層類開始遍歷所有類(規則3),對每個類,紀錄長整型和雙精度型、整型和浮點型、短整型和字元型、布林型和位元組型以及引用型別的非靜態欄位的個數。如果整型和雙精度型欄位個數不為0,且當前size沒有按長整型的大小對齊(規則5),選擇部分其他型別欄位排在長整型和雙精度型之前,直到填充到以長整型大小對齊,然後按照先規則2的順序排列個字計算不同型別欄位的大小。在每個類之間如果沒有按規定大小對齊,則填充缺少的位元組(規則4)。在所有類計算完成後,如果沒有按照類的對齊方式,則按類對齊規則對齊(規則1)。最後保證一個物件例項的大小要一個物件最小大小要大或相等。

  2. UnsafeSizeOf中
    UnsafeSizeOf的實現比反射的實現要簡單的多,它使用Sun內部庫的Unsafe類來獲取欄位的offset值來計算一個類佔用的記憶體大小(個人理解,這個應該只支援Sun JVM,但是怎麼JRockit中有對FieldOffsetAdjustment的配置,而該方法只在這個類中被使用。。。)。對陣列,它使用Unsafe.arrayBaseOffset()方法返回陣列頭大小,使用Unsafe.arrayIndexScale()方法返回一個數組元素佔用的記憶體大小,其他計算和反射機制類似。這裡在最後計算填充前有對FieldOffsetAdjustment的調整,貌似在JRockit JVM中使用到了,不瞭解為什麼它需要這個調整。對例項大小的計算也比較簡單,它首先遍歷當前類和父類的所有非靜態欄位,通過Unsafe.objectFieldOffset()找到最後一個欄位的offset,根據之前Java例項記憶體結構,要找到最後一個欄位,只需從當前類到最頂層父類遍歷第一個有非靜態欄位的類的所有非靜態欄位即可。在找到最後一個欄位的offset以後也需要做FieldOffsetAdjustment調整,之後還需要加1(因為有物件對齊大小對齊,因而通過加1而避免考慮最後一個欄位型別的問題,很巧妙的程式碼!)。最後根據規則以對物件以物件對齊大小對齊。

  3. AgentSizeOf
    在Java 1.5以後,提供了Instrumentation介面,可以呼叫該介面的getObjectSize方法獲取一個物件例項佔用的記憶體大小。對Instrumentation的機制不熟,但是從EHCache程式碼的實現角度上,它首先需要有一個sizeof-agent.jar的包(包含在net.sf.ehcache.pool.sizeof中),在該jar包的MANIFEST.MF檔案中指定Premain-Class類,這個類實現兩個靜態的premain、agentmain方法。在實際執行時,EHCache會將sizeof-agent.jar拷貝到臨時資料夾中,然後呼叫Sun工具包中的VirtualMachine的靜態attach方法,獲取一個VirtualMachine例項,然後呼叫其例項方法loadAgent方法,傳入sizeof-agent.jar檔案全路徑,即可將一個SizeOfAgent類附著到當前例項中,而我們就可以通過SizeOfAgent類來獲取它的Instrumentation例項來計算一個例項的大小。

我們可以使用一下一個簡單的例子來測試一下各種不同計算方法得出的結果: 

public class EhcacheSizeOfTest {
    public static void main(String[] args) {
        MyClass ins = new MyClass();
        
        System.out.println("ReflectionSizeOf: " + calculate(new ReflectionSizeOf(), ins));
        System.out.println("UnsafeSizeOf: " + calculate(new UnsafeSizeOf(), ins));
        System.out.println("AgentSizeOf: " + calculate(new AgentSizeOf(), ins));
    }
    
    private static long calculate(SizeOf sizeOf, Object instance) {
        return sizeOf.sizeOf(instance);
    }
    
    public static class MyClass {
        byte a;
        int c;
        boolean d;
        long e;
        Object f;
    }
}
//輸出結果如下(問題:這裡的JVM是64-Bit HotSpot JVM with Compressed OOPs,它的例項頭部佔用了12個位元組大小,但是它佔用記憶體的大小還是和32位的大小一樣,這是為什麼?):
[31 23:21:19,598 INFO ] [main] sizeof.JvmInformation - Detected JVM data model settings of: 64-Bit HotSpot JVM with Compressed OOPs
ReflectionSizeOf: 32
UnsafeSizeOf: 32
[31 23:26:52,479 INFO ] [main] sizeof.AgentLoader - Located valid 'tools.jar' at 'C:\Program Files\Java\jdk1.7.0_25\jre\..\lib\tools.jar'
[31 23:26:52,729 INFO ] [main] sizeof.AgentLoader - Extracted agent jar to temporary file C:\Users\DINGLE~1\AppData\Local\Temp\ehcache-sizeof-agent6171098352070763093.jar
[31 23:26:52,729 INFO ] [main] sizeof.AgentLoader - Trying to load agent @ C:\Users\DINGLE~1\AppData\Local\Temp\ehcache-sizeof-agent6171098352070763093.jar
AgentSizeOf: 32


Deep SizeOf計算
EHCache中的SizeOf類中還提供了deepSize計算,它的步驟是:使用ObjectGraphWalker遍歷一個例項的所有物件引用,在遍歷中通過使用傳入的SizeOfFilter過濾掉那些不需要的欄位,然後呼叫傳入的Visitor對每個需要計算的例項做計算。
ObjectGraphWalker的實現演算法和我之前所描述的類似,稍微不同的是它使用了Stack,我更傾向於使用Queue,只是這個也只是影響遍歷的順序,這裡有點深度優先還是廣度優先的味道。另外,它抽象了SizeOfFilter介面,可以用於過濾掉一些不想用於計算記憶體大小的欄位,如Element中的key欄位。SizeOfFilter提供了對類和欄位的過濾:

public interface SizeOfFilter {
    // Returns the fields to walk and measure for a type
    Collection<Field> filterFields(Class<?> klazz, Collection<Field> fields);
    // Checks whether the type needs to be filtered
    boolean filterClass(Class<?> klazz);
}

SizeOfFilter的實現類可以用於過濾過濾掉@IgnoreSizeOf註解的欄位和類,以及通過net.sf.ehcache.sizeof.filter系統變數定義的檔案,讀取其中的每一行為包名或欄位名作為過濾條件。最後,為了效能考慮,它對一些計算結果做了快取。

ObjectGraphWalker中,它還會忽略一些系統原本就存在的一些靜態變數以及類例項,所有這些資訊都定義在FlyweightType類中。

SizeOfEngine類
SizeOfEngine是EHCache中對使用不同方式做SizeOf計算的抽象,如在計算記憶體中物件的大小需要使用SizeOf類來實現,而計算磁碟中資料佔用的大小直接使用其size值即可,因而在EHCache中對SizeOfEngine有兩個實現:DefaultSizeOfEngine和DiskSizeOfEngine。對DiskSizeOfEngine比較簡單,其container引數必須是DiskMarker型別,並且直接返回其size欄位即可;對DefaultSizeOfEngine,則需要配置SizeOfFilter和SizeOf子類實現問題,對SizeOfFilter,它會預設加入AnnotationSizeOfFilter、使用builtin-sizeof.filter檔案中定義的類、欄位配置的ResourceSizeOfFilter、使用者通過net.sf.ehcache.sizeof.filter配置的filter檔案的ResourceSizeOfFilter;對SizeOf的子類實現問題,它優先選擇AgentSizeOf,如果不支援則使用UnsafeSizeOf,最後才使用ReflectionSizeOf。

public interface SizeOfEngine {
    Size sizeOf(Object key, Object value, Object container);
    SizeOfEngine copyWith(int maxDepth, boolean abortWhenMaxDepthExceeded);
}

還可以參考