分配堆外内存:在 JDK 28 中通常快 6-18 倍

分配堆外内存:在 JDK 28 中通常快 6-18 倍

💡 原文英文,约1400词,阅读约需5分钟。
📝

内容提要

自JDK 28起,FFM API的受限Arena小内存分配将使用原生内存池,分配延迟最多降低90%,无需修改代码且保留全部安全保证。每个平台线程可缓存四个64字节内存池,Arena首次分配时才获取,关闭后清零归还。虚拟线程临时借用载体线程的池,避免为每个虚拟线程建缓存。基准测试显示5字节和20字节分配速度提升显著,超过64字节的分配性能基本不变。

🔎

延伸解读

性能提升的边界:小分配受益,大分配不变

文章明确指出,池化优化仅对不超过64字节的小分配有效,超过此大小的分配仍走常规路径,性能基本持平。基准测试显示,5字节和20字节的分配延迟大幅降低,而100字节及以上的分配速度与未池化时相近。因此,这项改进主要惠及频繁进行微小堆外分配的场景,如FFM绑定、jextract生成的包装器以及短字符串传递。对于需要较大临时缓冲区的应用,收益有限。

虚拟线程的池借用机制:避免膨胀,保留隔离

为避免为每个虚拟线程创建原生内存缓存,虚拟线程在需要时临时从载体线程借用池,借用期间仅短暂固定,随后池脱离载体缓存并由Arena独占。这样虚拟线程在Arena打开期间可安全迁移,关闭时池归还到当前载体线程的缓存。该设计既维持了虚拟线程的小内存占用,又保证了池的独占所有权,但虚拟线程的分配仍比平台线程多出载体处理开销,不过相比未池化仍有显著提升。

底层分配行为变化:不要依赖malloc/free一一对应

池化后,关闭Arena可能将内存归还到JDK缓存而非立即调用原生释放函数,因此应用程序和诊断工具不应假设Arena分配与原生malloc/free调用存在一一对应关系。尽管段生命周期和访问规则不变,底层原生分配的生命周期可能不同。此外,池在重用前会清零,新池也初始化为零,这保留了Arena返回段包含零值的API要求,并防止数据泄露。清理动作在池清零和释放前运行,顺序得到保证。

❓

Q&A

JDK 28 对堆外内存分配做了哪些优化?

从 JDK 28 开始,FFM API 的受限 Arena 小内存分配将使用原生内存池,分配延迟最多降低 90%,无需修改代码且保留所有安全保证。

这个优化会影响哪些类型的 Arena?

该优化仅适用于受限 Arena(confined arenas),不影响其他类型的 Arena。

内存池是如何在平台线程和虚拟线程中管理的?

每个平台线程可惰性缓存最多四个 64 字节的内存池。虚拟线程不单独建缓存,而是临时从当前载体线程借用池,借用期间短暂固定,之后池归 Arena 独占,虚拟线程可安全迁移。

使用池化后,Arena 关闭时内存是如何处理的?

Arena 关闭时,池中使用过的部分会被清零并归还到线程缓存;如果缓存已满,则释放给原生分配器。清理动作在池被清零和释放之前运行。

池化优化对哪些分配大小的性能提升最明显?

对 5 字节和 20 字节等小分配提升显著(例如在 M4 Mac 上,5 字节分配从约 15.9 ns 降至约 1.1 ns),超过 64 字节的分配性能基本不变。

这个优化会改变 Arena 的内存生命周期或安全保证吗?

不会。池化不改变 Arena API 的生命周期或可访问性规则,仍保证确定性生命周期、零初始化和线程封闭性。但底层原生分配的释放时机可能不同,应用和诊断工具不应依赖分配与 malloc/free 的一一对应。

🏷️

标签

➡️

继续阅读