一、背景
offheap作爲擺脫gc的本地緩存來使用,對於緩存大量數據和提升應用的性能大有裨益。
EHCache的offheap層直接使用了Terracotta-OSS開源的offheap-store作爲底層實現。
但是offheap-store包含了一系列的算法和數據結構的設計和使用,很多地方借鑑了操作系統的知識,比如內存分頁設計,時鐘置換算法,內存分配等等,由此可見涉及到內存管理的都是比較複雜的領域,這裏僅僅是簡單介紹,爲後續深入使用鋪墊基礎。
想了解offheap-store的原因來自於以下幾個問題:
由於堆外內存不能存儲對象,只能存儲序列化後的二進制數據,那麼本質就轉換爲了數據對於內存的需求:
堆外內存如何分配和管理?
數據移除後內存如何釋放?
過期和剔除機制是怎樣的?
下面針對上面的問題進行一一說明。
二、ehcache offheap put
put時序圖,由於調用鏈太長,故分成兩部分,第一部分到OffHeapHashMap截止,如下:
此部分調用都是順序的,調用過程比較簡單,這裏只介紹一下EhcacheConcurrentOffHeapClockCache類:
-
EhcacheConcurrentOffHeapClockCache
它繼承了AbstractConcurrentOffHeapMap,因此有map的特性。其內部持有多個EhcacheSegment(每個Segment是一個OffHeapHashMap),每個Segment通過鎖來實現併發控制。如下圖所示:
第二部分時序圖如下:
這裏來詳細介紹一下每一步的調用:
-
第5步,OffHeapHashMap調用PortabilityBasedStorageEngine.writeMapping(k,v)寫入key和value。
-
第6步,PortabilityBasedStorageEngine調用OffHeapBufferStorageEngine.writeMappingBuffers(kb,vb)寫入key和value序列化後轉換爲ByteBuffer對應的值。
-
第7步,存儲數據到offheap之前需要先分配內存,這裏負責OffHeap內存分配的是OffHeapStorageArea。
-
第8步,OffHeapStorageArea調用UpfrontAllocatingPageSource.expand進行內存頁的分配,而UpfrontAllocatingPageSource初始化時會先將整體內存進行劃分爲塊(以大小爲10G的offheap爲例):
UpfrontAllocatingPageSource初始化時會按照最大爲1G大小拆分爲塊,這裏10G/1G=10,即會分爲10個大小爲1G的內存塊,如下圖所示:
-
第9步,內存頁分配藉助PowerOfTwoAllocator進行分配,其實現藉助於AA樹(紅黑樹的變種)。
-
第9步返回分配好的內存頁的起始地址。
到這裏說一下,內存頁分配示例圖:
上面圖示中1G大小的是已經預劃分好的內存塊,分配內存頁時會順序的在內存塊上分配出內存頁來,其分配釋放由PowerOfTwoAllocator來管理。
-
第8步返回分配好的內存頁對象,OffHeapStorageArea會將內存頁對象存儲在其內部hash表中。
-
第8.2步,OffHeapStorageArea會調用IntegerBestFitAllocator擴展內存,IntegerBestFitAllocator是內存管理器,其實現採用Doug Lea大神的內存分配器:dlmalloc。
-
第10步,經過第8,9步擴展好內存後,可以正式分配內存了,此時分配的內存大小就是實際需要的大小。
-
第10步返回實際需要的內存的起始地址。
-
第11步,返回正式的地址後,寫入數據,這裏需要說明一下,寫入的value將會包裝額外的元信息,包括:
long creationTime; long lastAccessTime; long expirationTime;
-
第12步,上面介紹的步驟都是爲了寫入key和value的數據,key的hash值也會寫入到offheap中,其實現主要在OffHeapHashMap中,它採用線性探測解決hash衝突。爲了避免大量key導致數據聚集,它在初始化時利用
UpfrontAllocatingPageSource
分配了hash表(堆外內存)來存儲key的hash值。一個key的hash值對應的空間稱作slot,共佔用16(int+int+long)個字節,並且當使用量大於50%時將進行自動擴容,如下:
每個slot存儲的數據結構如下:
int status marker;// 狀態值:0代表可用,1代表已使用,2代表已移除。
int cached key hashcode;//key的hash值。
long value address;//key對應的value存在offheap的地址。
下面從內存結構層面來描述一下整個映射過程:
-
假設offheap爲10個G,會首先劃分爲10個1個G的內存塊,之後所有內存管理都會以內存塊爲最大單位。
-
假設存儲的key爲字符串a,value爲一個對象video。首先介紹②key和value的內存空間,因爲key的hash值存儲中會存儲key和value實際存儲內存的地址。
-
②key和value的內存空間:
-
首先需要根據key和value的大小,從內存塊上擴展出能存儲下此大小的內存頁。
-
之後採用dlmalloc進行內存分配
-
分配完畢後,根據分配的內存起始地址寫入實際數據,數據結構如下:
writeInt(address, hash); writeInt(address + 4, keyLength); writeInt(address + 8, valueLength); writeBuffer(address + 12, keyBuffer); writeBuffer(address + 12 + keyLength, valueBuffer);
-
-
①key的hash值空間:
- 根據key的hash值97進行定位。
- 獲得key和value實際寫入的內存地址後,結合hash值,寫入hash表中。
根據put流程中的內存結構,很容易能夠知道get的流程,故get過程不再贅述。
三、ehcache offheap remove
remove主要涉及到內存的釋放,故流程圖只從OffHeapHashMap開始,前邊的EHCache相關的調用省略,如下:
- 第1步,EhcacheSegment調用OffHeapHashMap.computeIfPresentWithMetadata(k,fun)進行remove操作。
- 第2步,根據key定位hash表,獲得到value的內存地址。
- 第3,4,5,6都是順序調用,不再介紹。
- 第6步返回,IntegerBestFitAllocator之前說過,是採用dlmalloc算法實現的,它能判斷出是否需要釋放某頁。
- 第7步,如果需要釋放頁,會回調OffHeapStorageArea.free(page)進行內存頁的釋放。
- 第8步,內存頁由PowerOfTwoAllocator管理,調用free釋放內存頁。
當然,OffHeapHashMap還涉及到將slot(key的存儲內存)標記爲刪除,便於下次利用。
四、ehcache offheap evict
剔除發生在內存滿了但是還有數據寫入的時候,主要發生在如下兩種情況:
- hash表(存儲key的hashcode和value地址)滿了,對hash表擴容,但是擴容失敗。
- 存儲key和value的空間滿了,導致存儲失敗。
兩種剔除方式類似,都使用了clock eviction algorithm來通過hash表找到能夠剔除的slot,之後類似於remove操作,釋放映射的內存,並將hash表的slot標記爲刪除。
目前對於offheap層,不能選擇其他剔除方法,但可以提供建議供ehcache剔除時使用。
五、ehcache offheap expire
put和get時,均會檢測value中的元信息,如果過期,則執行類似remove的操作,釋放映射的內存。
六、總結
offheap層包含了一系列的算法和數據結構的設計和使用,很多地方借鑑了操作系統的知識,比如內存分頁設計,時鐘置換算法,內存分配等等,由此可見涉及到內存管理的都是比較複雜的領域,這裏僅僅是簡單介紹,爲後續深入使用鋪墊基礎。