内容提要
自JDK 28起,受限Arena(Arena.ofConfined())可直接从小型原生内存池分配内存,无需修改源码。每个平台线程默认缓存4个64字节内存池,首次分配时延迟获取,关闭后归还;虚拟线程临时借用载体线程的池,避免为每个虚拟线程建立缓存。该优化保持生命周期、零初始化和线程封闭语义,显著降低小分配延迟,适用于频繁调用原生代码的场景。
延伸解读
性能提升的适用范围
该优化仅对受限Arena(Arena.ofConfined())生效,其他Arena类型不受影响。从基准测试看,5字节和20字节的小分配延迟大幅降低,而超过默认64字节池大小的分配性能与之前基本持平,因为它们仍走常规分配路径。因此,收益主要集中在频繁进行微小原生分配的场景,如FFM绑定、jextract生成的包装器、小型C值/结构、指针输出参数、短字符串和字节数组等。
虚拟线程的池借用机制
为避免为每个虚拟线程创建原生内存缓存,虚拟线程会临时从其载体线程借用内存池。借用期间虚拟线程仅短暂固定,池随后从载体缓存分离并由Arena独占。因此虚拟线程在Arena打开期间可安全迁移,关闭时池会归还到当前载体线程的缓存。这既保留了池的独占所有权,又避免了为海量虚拟线程增加内存开销。
语义与生命周期不变
池化不改变Arena API的生命周期和可访问性规则:Arena关闭后其作用域失效、内存段不可访问。池在重用前会清零已使用部分,新分配的池也零初始化,确保返回的原生段包含零值,防止数据泄露。清理动作在池被清除和释放前运行,且缓存池在线程终止时确定性释放,打开Arena的池则与缓存分离,不会被意外释放。
对诊断工具的影响
虽然内存段生命周期不变,但底层原生分配的生命周期可能不同:关闭池化Arena可能将内存归还给JDK缓存,而非立即调用原生分配器的free。因此,应用程序和诊断工具不应依赖Arena分配与malloc/free调用之间的一一对应关系。这可能会影响那些假设每次Arena分配都对应一次原生分配的工具或监控逻辑。
Q&A
JDK 28 对 Arena.ofConfined() 做了哪些优化?
从 JDK 28 开始,受限 Arena 可以直接从小型原生内存池分配内存,无需修改源码。每个平台线程默认缓存 4 个 64 字节的内存池,首次分配时延迟获取,关闭后归还;虚拟线程临时借用载体线程的池,避免为每个虚拟线程建立缓存。该优化保持生命周期、零初始化和线程封闭语义,显著降低小分配延迟。
为什么需要为受限 Arena 引入内存池?
在 JDK 28 之前,即使很小的分配也可能涉及常规原生分配器和清理动作注册,对于只需要指针、long、int 或短字符串的原生调用,这种簿记开销可能超过访问内存本身。源码和运行时分析显示,超过 99.99% 的分配内存的受限 Arena 使用不到 64 字节,因此适合使用小型惰性池。
内存池是如何工作的?
每个平台线程可以惰性维护一个小型原生内存池缓存,默认保留 4 个 64 字节的池。受限 Arena 创建时不获取池,直到第一次适合池化的分配时才延迟获取。获取后,池专属于该 Arena,合适的分配从池中顺序切分。如果分配不适合或对齐要求无法满足,则走常规分配路径。Arena 关闭时,池的已用部分被清零并归还到线程缓存;如果缓存已满,则释放给原生分配器。
虚拟线程如何使用内存池?
虚拟线程不会为每个虚拟线程创建原生内存缓存,而是临时从其当前载体线程获取一个池。虚拟线程仅在短暂的缓存交接期间被固定,之后池从载体缓存分离并由 Arena 拥有。因此虚拟线程可以安全迁移,而 Arena 保持打开。当 Arena 关闭时,池返回到虚拟线程当时挂载的(可能不同的)载体线程的缓存中。
池化对 Arena 的生命周期和内存安全有什么影响?
池化不改变 Arena API 的生命周期或可访问性规则。Arena 关闭后,其作用域不再存活,内存段无法访问。在池可供重用之前,前一个 Arena 使用的部分会被清零,新分配的池也进行零初始化,这保留了 API 要求(Arena 返回的原生段包含零),并防止一个 Arena 写入的数据通过后续 Arena 暴露。常规清理动作在池被清零和释放之前运行。
池化优化带来了多少性能提升?
开发基准测试显示,对于微小的受限分配,延迟大幅降低。在 Apple M4 上,5 字节分配从 15.862 ns/op 降至 1.052 ns/op,20 字节从 18.139 ns/op 降至 1.138 ns/op。对于超过默认 64 字节池大小的分配,性能大致持平,因为这些分配使用常规路径。虚拟线程也有显著改进,例如 5 字节分配从 15.672 ns/op 降至 3.222 ns/op。
这个优化适用于哪些场景?
该优化特别适用于:围绕每个原生调用创建受限 Arena 的 FFM 绑定、生成的 jextract 风格包装器、小型 C 值和结构、原生句柄和指针输出参数、传递给原生代码的短字符串和字节数组,以及频繁进行廉价原生调用的延迟敏感代码。注意,这些好处仅适用于受限 Arena,不适用于其他 Arena 类型。