Ehcache計算Java對象內存大小
在EHCache中,可以設置maxBytesLocalHeap、maxBytesLocalOffHeap、maxBytesLocalDisk值,以控制Cache占用的內存、磁盤的大小(註:這裏Off Heap是指Element中的值已被序列化,但是還沒寫入磁盤的狀態,貌似只有企業版的EHCache支持這種配置;而這裏maxBytesLocalDisk是指在最大在磁盤中的數據大小,而不是磁盤文件大小,因為磁盤文中有一些數據是空閑區),因而EHCache需要有一種機制計算一個類在內存、磁盤中占用的字節數,其中在磁盤中占用的字節大小計算比較容易,只需要知道序列化後字節數組的大小,並且加上一些統計信息,如過期時間、磁盤位置、命中次數等信息即可,而要計算一個對象實例在內存中占用的大小則要復雜一些。
計算一個實例內存占用大小思路
在Java中,除了基本類型,其他所有通過字段包含其他實例的關系都是引用關系,因而我們不能直接計算該實例占用的內存大小,而是要遞歸的計算其所有字段占用的內存大小的和。在Java中,我們可以將所有這些通過字段引用簡單的看成一種樹狀結構,這樣就可以遍歷這棵樹,計算每個節點占用的內存大小,所有這些節點占用的內存大小的總和就當前實例占用的內存大小,遍歷的算法有:先序遍歷、中序遍歷、後序遍歷、層級遍歷等。但是在實際情況中很容易出現環狀引用(最簡單的是兩個實例之間的直接引用,還有是多個實例構成的一個引用圈),而破壞這種樹狀結構,而讓引用變成圖狀結構。然而圖的遍歷相對比較復雜(至少對我來說),因而我更願意把它繼續看成一顆樹狀圖,采用層級遍歷,通過一個IdentitySet紀錄已經計算過的節點(實例),並且使用一個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個字節,引用類型的數組,先計算每個引用元素占用的字節數,然後是引用本省占用的字節數。
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個字節的內存!
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字節對齊的情況小,效率要高的多。
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處理父類中的成員,接著才是子類的成員。
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個字節的基本單位。
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)的順序向未填滿的空間填充。
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中計算一個實例占用內存大小的基本思路和以上類似:遍歷實例數上的所有節點,對每個節點計算其占用的內存大小。不過它結構設計的更好,而且它有三種用於計算一個實例占用內存大小的實現。我們先來看這三種用於計算一個實例占用內存大小的邏輯:
- 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)。最後保證一個對象實例的大小要一個對象最小大小要大或相等。 - UnsafeSizeOf中
UnsafeSizeOf的實現比反射的實現要簡單的多,它使用Sun內部庫的Unsafe類來獲取字段的offset值來計算一個類占用的內存大小(個人理解,這個應該只支持Sun JVM,但是怎麽JRockit中有對FieldOffsetAdjustment的配置,而該方法只在這個類中被使用。。。)。對數組,它使用Unsafe.arrayBaseOffset()方法返回數組頭大小,使用Unsafe.arrayIndexScale()方法返回一個數組元素占用的內存大小,其他計算和反射機制類似。這裏在最後計算填充前有對FieldOffsetAdjustment的調整,貌似在JRockit JVM中使用到了,不了解為什麽它需要這個調整。對實例大小的計算也比較簡單,它首先遍歷當前類和父類的所有非靜態字段,通過Unsafe.objectFieldOffset()找到最後一個字段的offset,根據之前Java實例內存結構,要找到最後一個字段,只需從當前類到最頂層父類遍歷第一個有非靜態字段的類的所有非靜態字段即可。在找到最後一個字段的offset以後也需要做FieldOffsetAdjustment調整,之後還需要加1(因為有對象對齊大小對齊,因而通過加1而避免考慮最後一個字段類型的問題,很巧妙的代碼!)。最後根據規則以對對象以對象對齊大小對齊。 - 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 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提供了對類和字段的過濾:
// 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。
Size sizeOf(Object key, Object value, Object container);
SizeOfEngine copyWith(int maxDepth, boolean abortWhenMaxDepthExceeded);
}
還可以參考
Ehcache計算Java對象內存大小