ehcache之offheap
一、背景
Off-Heap(堆外内存)作为摆脱 GC(Garbage Collection)影响的本地缓存方案,对于缓存大量数据及提升应用性能大有裨益。
EHCache 的 Off-Heap 层直接使用了 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 步:存储数据到 Off-Heap 之前需要先分配内存,此处负责 Off-Heap 内存分配的是
OffHeapStorageArea。 第 8 步:
OffHeapStorageArea调用UpfrontAllocatingPageSource.expand进行内存页的分配。UpfrontAllocatingPageSource初始化时会先将整体内存划分为块。以大小为 10G 的 Off-Heap 为例,初始化时会按照最大 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 值也会写入到 Off-Heap 中,其实现主要在
OffHeapHashMap中,它采用 线性探测 解决 hash 冲突。为了避免大量 key 导致数据聚集,它在初始化时利用UpfrontAllocatingPageSource分配了 hash 表(堆外内存)来存储 key 的 hash 值。一个 key 的 hash 值对应的空间称作 slot,共占用 16 (int+int+long) 个字节,并且当使用量大于 50% 时将进行自动扩容,如下:

每个 slot 存储的数据结构如下:
```java
int status marker; // 状态值:0 代表可用,1 代表已使用,2 代表已移除。
int cached key hashcode;// key 的 hash 值。
long value address; // key 对应的 value 存在 offheap 的地址。
```
下面从内存结构层面来描述一下整个映射过程:

- 假设 Off-Heap 为 10 个 G,会首先划分为 10 个 1G 的内存块,之后所有内存管理都会以内存块为最大单位。
- 假设存储的 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
剔除(Eviction)发生在内存满了但是还有数据写入的时候,主要发生在如下两种情况:
- hash 表(存储 key 的 hashcode 和 value 地址)满了,对 hash 表扩容,但是扩容失败。
- 存储 key 和 value 的空间满了,导致存储失败。
两种剔除方式类似,都使用了 clock eviction algorithm 来通过 hash 表找到能够剔除的 slot,之后类似于 remove 操作,释放映射的内存,并将 hash 表的 slot 标记为删除。
目前对于 offheap 层,不能选择其他剔除方法,但可以提供 建议 供 ehcache 剔除时使用。
五、ehcache offheap expire
PUT 和 GET 时,均会检测 value 中的元信息,如果过期,则执行类似 REMOVE 的操作,释放映射的内存。
六、总结
Off-Heap 层包含了一系列复杂的算法和数据结构设计,大量借鉴了操作系统的知识,如内存分页设计、时钟置换算法、内存分配器等。内存管理本身是一个较为复杂的领域,本文仅对其核心流程进行了简单介绍,旨在为后续深入使用 Ehcache Off-Heap 功能铺垫基础。
说明:本文内容基于 Ehcache 3.x 版本(参考文档版本 3.8)及对应的 offheap-store 实现,具体细节可能随版本迭代有所调整。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/ehcache-zhi-offheap.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。