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個字節的內存!
html

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實例來計算一個實例的大小。

咱們可使用一下一個簡單的例子來測試一下各類不一樣計算方法得出的結果: 
ui

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提供了對類和字段的過濾:
spa

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

還能夠參考

相關文章
相關標籤/搜索