一、背景

Off-Heap(堆外内存)作为摆脱 GC(Garbage Collection)影响的本地缓存方案,对于缓存大量数据及提升应用性能大有裨益。

EHCache 的 Off-Heap 层直接使用了 Terracotta-OSS 开源的 offheap-store 作为底层实现。

offheap-store 包含了一系列算法和数据结构的设计,很多地方借鉴了操作系统的知识,例如内存分页设计、时钟置换算法、内存分配等。由此可见,涉及内存管理的领域都比较复杂。本文仅作简单介绍,旨在为后续深入使用铺垫基础。

关注 offheap-store 的实现主要源于以下几个核心问题:

由于堆外内存不能直接存储对象,只能存储序列化后的二进制数据,本质上转换为了数据对内存的需求管理:

  1. 堆外内存如何分配和管理?
  2. 数据移除后内存如何释放?
  3. 过期和剔除机制是怎样的?

下文将针对这些问题逐一说明。

二、ehcache offheap put

PUT 操作时序图由于调用链较长,分为两部分展示。第一部分到 OffHeapHashMap 截止,如下:

PUT 时序图第一部分

此部分调用均为顺序执行,过程相对简单。这里重点介绍一下 EhcacheConcurrentOffHeapClockCache 类:

  • EhcacheConcurrentOffHeapClockCache
    它继承了 AbstractConcurrentOffHeapMap,因此具备 Map 的特性。其内部持有多个 EhcacheSegment(每个 Segment 是一个 OffHeapHashMap),每个 Segment 通过锁来实现并发控制。如下图所示:

EhcacheConcurrentOffHeapClockCache 结构

第二部分时序图如下:

PUT 时序图第二部分

下面详细介绍每一步的调用逻辑:

  • 第 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 结构扩容

每个 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 相关调用省略,如下:

REMOVE 流程图

  • 第 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)发生在内存满了但是还有数据写入的时候,主要发生在如下两种情况:

  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 的操作,释放映射的内存。

六、总结

Off-Heap 层包含了一系列复杂的算法和数据结构设计,大量借鉴了操作系统的知识,如内存分页设计、时钟置换算法、内存分配器等。内存管理本身是一个较为复杂的领域,本文仅对其核心流程进行了简单介绍,旨在为后续深入使用 Ehcache Off-Heap 功能铺垫基础。

说明:本文内容基于 Ehcache 3.x 版本(参考文档版本 3.8)及对应的 offheap-store 实现,具体细节可能随版本迭代有所调整。