ehcache之offheap

一、背景

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

剔除發生在內存滿了但是還有數據寫入的時候,主要發生在如下兩種情況:

  1. hash表(存儲key的hashcode和value地址)滿了,對hash表擴容,但是擴容失敗。
  2. 存儲key和value的空間滿了,導致存儲失敗。

兩種剔除方式類似,都使用了clock eviction algorithm來通過hash表找到能夠剔除的slot,之後類似於remove操作,釋放映射的內存,並將hash表的slot標記爲刪除。

目前對於offheap層,不能選擇其他剔除方法,但可以提供建議供ehcache剔除時使用。

五、ehcache offheap expire

put和get時,均會檢測value中的元信息,如果過期,則執行類似remove的操作,釋放映射的內存。

六、總結

offheap層包含了一系列的算法和數據結構的設計和使用,很多地方借鑑了操作系統的知識,比如內存分頁設計,時鐘置換算法,內存分配等等,由此可見涉及到內存管理的都是比較複雜的領域,這裏僅僅是簡單介紹,爲後續深入使用鋪墊基礎。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章