Class AdaptivePoolingAllocator.ThreadLocalSizeClassedChunkCache

  • All Implemented Interfaces:
    AdaptivePoolingAllocator.ChunkCache
    Enclosing class:
    AdaptivePoolingAllocator

    static final class AdaptivePoolingAllocator.ThreadLocalSizeClassedChunkCache
    extends AdaptivePoolingAllocator.SizeClassedChunkCache
    Ring buffer cache for thread-local chunk reuse (SPSC — only the owner thread accesses it).

    Logical layout after purge:

       head                          tail
       v                             v
       [..., notEmpty, notEmpty, ..., empty, empty, ..., null, ...]
            |--- notEmptyCount ---|--- emptyCount --|
            |------------ count ------------------|
     

    Physical layout when the ring wraps:

       0         tail          head          length
       v         v             v             v
       [...tail] [  unused  ]  [head................]
                 ^             |--- content wraps ---|
                 wrap point
     

    scanForCapacity — O(1) fast path takes from head while notEmptyCount > 0:

       before: notEmptyCount=2, count=5
       [NE, NE, E, E, E, _, _, _]
        ^head            ^tail
    
       after: returns NE, notEmptyCount=1, count=4
       [_,  NE, E, E, E, _, _, _]
            ^head        ^tail
     
    Fallback when notEmptyCount == 0: linear scan of the empty zone for chunks that gained capacity from external segment returns.

    offerChunk — write at tail, grow (double + linearize) if full:

       before: count=4
       [_,  NE, E, E, E, _, _, _]
            ^head        ^tail
    
       after: count=5
       [_,  NE, E, E, E, X, _, _]
            ^head           ^tail
     

    runPurgeScan (every AdaptivePoolingAllocator.CHUNK_PURGE_POLLS_THREAD_LOCAL polls) — two passes. Pass 1: age idle chunks (full → epoch++, non-full → epoch=0), evict past threshold, compact survivors (nulls stale slots inline). Pass 2: partition hasCap to front / noCap to back, then three-way Dutch-flag within hasCap into [epoch=0 | 0<epoch<threshold | epoch>=threshold]. Chunks with epoch>=threshold are placed at the back of hasCap so scan doesn't reach them — they age to threshold+1 and get evicted. Never selects — selection is always scanForCapacity.

    Case 1 — no eviction, an empty chunk gained capacity externally (common):

       before (E* gained capacity since last purge):
       [NE, NE, E*, E, _, _, _, _]
        ^head            ^tail
        notEmptyCount=2
    
       pass 1: age idle chunks. None past threshold. No compaction needed.
       pass 2 (partition): E* now has capacity → placed in notEmpty zone.
    
       after:
       [NE, NE, E*, E, _, _, _, _]
        ^head            ^tail
        notEmptyCount=3
     

    Case 2 — eviction (uncommon, burst wind-down):

       before (ring wraps, IDLE* = idle past threshold):
       [E, NE, _,  IDLE*, NE, E, E, NE]
              ^tail ^head
    
       pass 1: IDLE* evicted (markToDeallocate), survivors compacted, stale slots nulled.
       [_, _, _,  NE, E, E, NE, E]
         ^tail    ^head
                  |--- kept=6 ---|
    
       pass 2 (partition): [epoch=0 hasCap | 0<epoch<T hasCap | epoch>=T hasCap | noCap].
       [_, _, _,  NE, NE, E, E, E]
         ^tail    ^head
                  notEmptyCount=2, count=6
     
    Idle chunks (remainingCapacity == capacity) age via purgeEpoch and are evicted past threshold, but at least purgeRetentionFloor chunks are always retained.