On Thu, 25 Jun 2026 10:22:22 GMT, Per Minborg <[email protected]> wrote:

>> ## Summary
>> 
>> This PR proposes to introduce a pooled confined arena as an optimization for 
>> `Arena.ofConfined()`, where small native allocations can be served from a 
>> reusable per-thread/per-slot memory pool instead of calling the regular 
>> native allocator for every short-lived arena. The arena remains confined to 
>> its owner thread and is still closed normally, but its backing storage can 
>> be reset and reused when the arena closes. The feature requires no API 
>> changes.
>> 
>> ### Outline
>> 
>> Platform threads: There are up to four lazily allocated pools per Thread, 
>> encoded in `Thread.confinedMemoryPool`.
>> Virtual threads: fixed shared native pool with CAS-protected slots, because 
>> per-virtual-thread native pools would not scale.
>> 
>> Pooled memory is zeroed out upon _closing_ an Arena to minimize data 
>> visibility between reuse. This means the data is visible only within a TWR 
>> block, and never outside it.
>> 
>> By default, a confined arena has access to four pools, each of size 64 
>> bytes.  The pool sizes are configurable via a system property and can be 8, 
>> 16, 32, or 64 bytes. Pooling can also be turned off completely by setting 
>> the pool power-of-two size to zero. As there can be up to four pools per 
>> thread, nested confined arenas are supported (i.e., up to four nested 
>> arenas).
>> 
>> ## Static Analysis
>> 
>> An extensive static corpus analysis of third-party libraries and the JDK 
>> itself has been conducted with respect to `Area.ofConfined()` usage, 
>> revealing that confined arenas were used _only_ in TWR blocks and _never_ in 
>> an unstructured way. The static analysis further revealed that in most 
>> cases, only a small amount of native memory was ever allocated, usually less 
>> than 32 bytes, and in many cases, 8 bytes or less. This usage pattern lends 
>> itself well to pooling. 
>> 
>> ## Dynamic Analysis
>> 
>> A dynamic statistical analysis of actual runs was also made, where various 
>> properties of confined arenas were recorded and summarized during a complete 
>> tier1 test run. While a tier1 run is not necessarily representative of a 
>> typical application workload, it provided some interesting results:
>> 
>> The run produced 93 per-process histogram blocks and 788,773,092 closed 
>> confined arenas. The result is dominated by arenas with no native allocation 
>> at all: 375,934,768 arenas (47.661%) are in the zero-byte bucket. Counting 
>> arenas up to 63 bytes covers 99.997% of all arena closures.
>> 
>> The largest count bucket is 8-15 bytes per arena with 400,951,293 arenas 
>> (50.832% of all arenas...
>
> Per Minborg has updated the pull request incrementally with two additional 
> commits since the last revision:
> 
>  - Add local pools
>  - Add 4 nested levels

I have pushed a substantial refactoring of the confined arena pooling 
implementation. The goal and API surface are unchanged, but the ownership model 
has changed enough that the current revision should be reviewed as a new 
implementation.

The main changes are:
 * Pooling logic is centralized in `ConfinedSegmentPool`, while the allocation 
fast path is implemented by `ArenaImpl.OfConfined`.
 * Each platform thread can lazily allocate a four-entry pool cache in 
`Thread.FieldHolder`. Threads that never use pooled confined arenas do not 
allocate this array.
 * No additional fields are added to `VirtualThread`.
 * A platform-thread arena *marks* an acquired cache entry as negative, 
allowing thread-exit cleanup to find and free both available and acquired pools.
 * A virtual-thread arena acquires from its current carrier by *removing* the 
pool from that carrier’s cache. The arena then owns the pool independently of 
the carrier. This prevents carrier migration or termination from causing a 
use-after-free or double-free.
 * When a virtual-thread arena closes, its pool is returned to the current 
carrier’s cache, or freed if that cache is full.
 * If no cached pool is available, the arena allocates a local pool. On close, 
that pool is either cached or freed. This also allows nested arenas beyond the 
cache capacity to retain the small-allocation fast path.
 
The important ownership invariants are:
 * A pool has exactly one owner at any time.
 * A virtual-thread-owned or locally allocated pool is absent from all thread 
caches.
 * Used memory is zeroed _before_ the pool is published for reuse.
 * The arena session is invalidated _before_ its pool can be reused.
* Platform-thread termination frees every positive or negative pool entry still 
recorded in its cache.
 * Allocations that do not fit in the pool, or whose alignment exceeds the pool 
size, use the regular allocator. Pooling can still be disabled using the 
internal pool-size property.

The tests have been expanded to cover platform and virtual threads, nested and 
out-of-order arena closure, fallback allocation, zeroing, stale-segment access, 
cache saturation, thread-exit cleanup, invalid release attempts, configuration 
parsing, and forced virtual-thread migration between dedicated carriers, 
including termination of the original carrier.

Because this replaces several previously reviewed implementation details, I 
would appreciate a fresh review of the current diff, particularly the ownership 
transitions, thread-exit cleanup, and virtual-thread migration protocol.

Early performance runs indicate on-par performance for platform threads and 
significantly better performance for virtual threads owing to the removal of 
CAS operations.

-------------

PR Comment: https://git.openjdk.org/jdk/pull/31365#issuecomment-5244564377

Reply via email to