在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位元組對齊的情況小,效率要高的多。
int c;
boolean d;
long e;
object f;
//如果jvm不對其重排序,它要占40個位元組
[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;
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 b;
[b: 1 byte ] 13
[padding: 3 bytes] 16
規則5: 如果子類第一個成員時一個雙精度或長整型,并且父類沒有用完8個位元組,jvm會破壞規則2,按整型(int)、短整型(short)、位元組型(byte)、引用類型(reference)的順序向未填滿的空間填充。
long b;
short c;
byte d;
[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
8
hotspot 32-bit with concurrent mark-and-sweep gc
16
hotspot 64-bit
hotspot 64-bit with concurrent mark-and-sweep gc
24
hotspot 64-bit with compressed oops
12
hotspot 64-bit with compressed oops and concurrent mark-and-sweep gc
jrockit 32-bit
jrockit 64-bit(with no reference compression)
jrockit 64-bit with 4gb compressed references
jrockit 64-bit with 32gb compressed references
jrockit 64-bit with 64gb compressed references
ibm 64-bit with compressed references
ibm 64-bit with no reference compression
ibm 32-bit
unknown 32-bit
unknown 64-bit
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 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);
http://www.javamex.com/tutorials/memory/