在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
規則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字節對齊的狀況小,效率要高的多。
算法
規則3: 不一樣類繼承關係中的成員不能混合排列。首先按照規則2處理父類中的成員,接着纔是子類的成員。
數組
規則4: 當父類最後一個屬性和子類第一個屬性之間間隔不足4字節時,必須擴展到4個字節的基本單位。
緩存
規則5: 若是子類第一個成員時一個雙精度或長整型,而且父類沒有用完8個字節,JVM會破壞規則2,按整型(int)、短整型(short)、字節型(byte)、引用類型(reference)的順序向未填滿的空間填充。
工具
數組內存佈局
數組對象除了做爲對象而存在的頭之外,還存在一個額外的頭部成員用來存放數組的長度,它佔4個字節。
佈局
非靜態內部類
非靜態內不累它又一個額外的「隱藏」成員,這個成員時一個指向外部類的引用變量。這個成員是一個普通引用,所以遵循引用內存佈局的規則。所以內部類有4個字節的額外開銷。
EHCache計算一個實例佔用的內存大小
EHCache中計算一個實例佔用內存大小的基本思路和以上相似:遍歷實例數上的全部節點,對每一個節點計算其佔用的內存大小。不過它結構設計的更好,並且它有三種用於計算一個實例佔用內存大小的實現。咱們先來看這三種用於計算一個實例佔用內存大小的邏輯:
性能
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測試
咱們可使用一下一個簡單的例子來測試一下各類不一樣計算方法得出的結果:
ui
Deep SizeOf計算
EHCache中的SizeOf類中還提供了deepSize計算,它的步驟是:使用ObjectGraphWalker遍歷一個實例的全部對象引用,在遍歷中經過使用傳入的SizeOfFilter過濾掉那些不須要的字段,而後調用傳入的Visitor對每一個須要計算的實例作計算。
ObjectGraphWalker的實現算法和我以前所描述的相似,稍微不一樣的是它使用了Stack,我更傾向於使用Queue,只是這個也只是影響遍歷的順序,這裏有點深度優先仍是廣度優先的味道。另外,它抽象了SizeOfFilter接口,能夠用於過濾掉一些不想用於計算內存大小的字段,如Element中的key字段。SizeOfFilter提供了對類和字段的過濾:
spa
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。