Serguey Asael Shinder: JDK 28 serves small Arena.ofConfined() allocations from reusable native pools
An Inside Java post from 5 October describes a change to the Foreign Function and Memory API in JDK 28: small allocations in a confined arena are now served from reusable native-memory pools. Applications get it without changing a line of code.
The pattern it targets. Confined arenas are typically used as scratch space around a native call:
try (Arena arena = Arena.ofConfined()) {
MemorySegment result = arena.allocate(ValueLayout.JAVA_INT);
nativeFunction.invokeExact(result); // writes a result into the segment
}

Before JDK 28, even an allocation of a few bytes went through the regular native allocator and registered a cleanup action. For a call that only needs a pointer, a long, an int or a short string, the post says, that bookkeeping can cost more than using the memory. In one instrumented test-suite run, more than 99.99% of confined arenas that allocated anything used less than 64 bytes, and some allocated nothing at all.
How the pools work.
- Each platform thread lazily keeps a small cache: by default four pools of 64 bytes.
- An arena takes a pool only on its first allocation that can use it, so arenas that never allocate cost nothing. Once taken, the pool belongs to that arena alone, which keeps nested arenas isolated.
- Allocations are carved sequentially from the pool. Anything that does not fit, or needs an alignment the pool cannot guarantee, takes the old path.
- Virtual threads do not get their own cache. A virtual thread briefly borrows a pool from its carrier, is pinned only for that handoff, and can then migrate freely; on close the pool goes back to whichever carrier it is mounted on.
What does not change. After close() the arena's scope is dead and its segments are inaccessible, as before. The used part of a pool is zeroed before reuse, so data from one arena cannot appear in another, and new pools are zero-initialised, which keeps the guarantee that arena memory starts as zeroes. Cleanup actions run before the pool is cleared.
What does change. Closing an arena may return memory to the JDK's cache instead of calling free. The post is explicit that applications and diagnostic tools must not assume one native malloc/free per arena allocation.
The numbers. On an Apple M4 running macOS, the post reports, for a virtual thread, a 5-byte confined allocation at about 3.2 ns with pooling against 15.7 ns without, and a 20-byte one at about 2.7 ns against 18.1 ns. At 100 bytes and above, beyond the 64-byte pool, results are at parity because those allocations take the regular path. Benchmark details are in OpenJDK PR #31365. The authors add the usual warning that microbenchmark speedups do not translate directly into application speedups; the gain is largest where creating the arena and allocating are a big share of a short native call.
No JDK runs on the machine this note was written on; the figures are those published in the linked post.