View Javadoc
1   /*
2    * Copyright 2022 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.buffer;
17  
18  import io.netty.util.ByteProcessor;
19  import io.netty.util.CharsetUtil;
20  import io.netty.util.IllegalReferenceCountException;
21  import io.netty.util.NettyRuntime;
22  import io.netty.util.Recycler;
23  import io.netty.util.Recycler.EnhancedHandle;
24  import io.netty.util.concurrent.ConcurrentSkipListIntObjMultimap;
25  import io.netty.util.concurrent.ConcurrentSkipListIntObjMultimap.IntEntry;
26  import io.netty.util.concurrent.FastThreadLocal;
27  import io.netty.util.concurrent.FastThreadLocalThread;
28  import io.netty.util.concurrent.MpscIntQueue;
29  import io.netty.util.internal.MathUtil;
30  import io.netty.util.internal.ObjectUtil;
31  import io.netty.util.internal.PlatformDependent;
32  import io.netty.util.internal.RefCnt;
33  import io.netty.util.internal.SystemPropertyUtil;
34  import io.netty.util.internal.ThreadExecutorMap;
35  import io.netty.util.internal.UnstableApi;
36  
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.nio.ByteBuffer;
41  import java.nio.ByteOrder;
42  import java.nio.channels.ClosedChannelException;
43  import java.nio.channels.FileChannel;
44  import java.nio.channels.GatheringByteChannel;
45  import java.nio.channels.ScatteringByteChannel;
46  import java.nio.charset.Charset;
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.Iterator;
50  import java.util.Queue;
51  import java.util.concurrent.atomic.AtomicInteger;
52  import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
53  import java.util.concurrent.atomic.AtomicLong;
54  import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
55  import java.util.concurrent.atomic.LongAdder;
56  import java.util.concurrent.locks.StampedLock;
57  import java.util.function.IntConsumer;
58  
59  /**
60   * An auto-tuning pooling allocator, that follows an anti-generational hypothesis.
61   * <p>
62   * The allocator is organized into a list of Magazines, and each magazine has a chunk-buffer that they allocate buffers
63   * from.
64   * <p>
65   * The magazines hold the mutexes that ensure the thread-safety of the allocator, and each thread picks a magazine
66   * based on the id of the thread. This spreads the contention of multi-threaded access across the magazines.
67   * If contention is detected above a certain threshold, the number of magazines are increased in response to the
68   * contention.
69   * <p>
70   * The magazines maintain histograms of the sizes of the allocations they do. The histograms are used to compute the
71   * preferred chunk size. The preferred chunk size is one that is big enough to service 10 allocations of the
72   * 99-percentile size. This way, the chunk size is adapted to the allocation patterns.
73   * <p>
74   * Computing the preferred chunk size is a somewhat expensive operation. Therefore, the frequency with which this is
75   * done, is also adapted to the allocation pattern. If a newly computed preferred chunk is the same as the previous
76   * preferred chunk size, then the frequency is reduced. Otherwise, the frequency is increased.
77   * <p>
78   * This allows the allocator to quickly respond to changes in the application workload,
79   * without suffering undue overhead from maintaining its statistics.
80   * <p>
81   * Since magazines are "relatively thread-local", the allocator has a chunk cache that allows excess chunks from any
82   * magazine to be shared with other magazines.
83   */
84  @UnstableApi
85  final class AdaptivePoolingAllocator {
86      private static final int LOW_MEM_THRESHOLD = 512 * 1024 * 1024;
87      private static final boolean IS_LOW_MEM = SystemPropertyUtil.getBoolean(
88              "io.netty.allocator.lowMemory",
89              Runtime.getRuntime().maxMemory() <= LOW_MEM_THRESHOLD);
90  
91      /**
92       * Whether the IS_LOW_MEM setting should disable thread-local magazines.
93       * This can have fairly high performance overhead.
94       */
95      private static final boolean DISABLE_THREAD_LOCAL_MAGAZINES_ON_LOW_MEM = SystemPropertyUtil.getBoolean(
96              "io.netty.allocator.disableThreadLocalMagazinesOnLowMemory", true);
97  
98      /**
99       * The 128 KiB minimum chunk size is chosen to encourage the system allocator to delegate to mmap for chunk
100      * allocations. For instance, glibc will do this.
101      * This pushes any fragmentation from chunk size deviations off physical memory, onto virtual memory,
102      * which is a much, much larger space. Chunks are also allocated in whole multiples of the minimum
103      * chunk size, which itself is a whole multiple of popular page sizes like 4 KiB, 16 KiB, and 64 KiB.
104      */
105     static final int MIN_CHUNK_SIZE = 128 * 1024;
106     private static final int EXPANSION_ATTEMPTS = 3;
107     private static final int INITIAL_MAGAZINES = 1;
108     private static final int RETIRE_CAPACITY = 256;
109     private static final int MAX_STRIPES = IS_LOW_MEM ? 1 : NettyRuntime.availableProcessors() * 2;
110     private static final int BUFS_PER_CHUNK = 8; // For large buffers, aim to have about this many buffers per chunk.
111 
112     /**
113      * The maximum size of a pooled chunk, in bytes. Allocations bigger than this will never be pooled.
114      * <p>
115      * This number is 8 MiB, and is derived from the limitations of internal histograms.
116      */
117     private static final int MAX_CHUNK_SIZE = IS_LOW_MEM ?
118             2 * 1024 * 1024 : // 2 MiB for systems with small heaps.
119             8 * 1024 * 1024; // 8 MiB.
120     private static final int MAX_POOLED_BUF_SIZE = MAX_CHUNK_SIZE / BUFS_PER_CHUNK;
121 
122     /**
123      * The capacity if the chunk reuse queues, that allow chunks to be shared across magazines in a group.
124      * The default size is twice {@link NettyRuntime#availableProcessors()},
125      * same as the maximum number of magazines per magazine group.
126      */
127     static final int CHUNK_REUSE_QUEUE = Math.max(2, SystemPropertyUtil.getInt(
128             "io.netty.allocator.chunkReuseQueueCapacity", NettyRuntime.availableProcessors() * 2));
129 
130     static final long CHUNK_PURGE_POLLS_THREAD_LOCAL = Math.max(1, SystemPropertyUtil.getLong(
131             "io.netty.allocator.chunkPurgePollsThreadLocal", 16L));
132 
133     static final long CHUNK_PURGE_POLLS_SHARED = Math.max(1, SystemPropertyUtil.getLong(
134             "io.netty.allocator.chunkPurgePollsShared", 128L));
135 
136     static final int CHUNK_PURGE_THRESHOLD = Math.max(1, SystemPropertyUtil.getInt(
137             "io.netty.allocator.chunkPurgeThreshold", 3));
138 
139     /**
140      * Per-size-class upper bound (in bytes) on the thread-local chunk cache.
141      * When a size class cache holds this many bytes worth of chunks,
142      * further offers are rejected and the chunk is marked for immediate deallocation.
143      * Chunks already in the cache are only evicted by the purge mechanism (they must be full and idle for
144      * {@link #CHUNK_PURGE_THRESHOLD} consecutive purge cycles).
145      */
146     static final int THREAD_LOCAL_CACHE_MAX_BYTES = Math.max(1, SystemPropertyUtil.getInt(
147             "io.netty.allocator.threadLocalChunkCacheMaxBytes", 8 * 1024 * 1024));
148 
149     /**
150      * Per-size-class lower bound (in bytes) on the thread-local chunk cache.
151      * The purge mechanism will not evict chunks below this retention floor, even if they are full and idle.
152      * Clamped to {@link #THREAD_LOCAL_CACHE_MAX_BYTES} if the configured value exceeds it.
153      * When equal to {@link #THREAD_LOCAL_CACHE_MAX_BYTES}, purge eviction is effectively disabled.
154      */
155     static final int THREAD_LOCAL_CACHE_MIN_BYTES = Math.min(THREAD_LOCAL_CACHE_MAX_BYTES,
156             Math.max(1, SystemPropertyUtil.getInt(
157                     "io.netty.allocator.threadLocalChunkCacheMinBytes",
158                     THREAD_LOCAL_CACHE_MAX_BYTES / 2)));
159 
160     /**
161      * The capacity if the magazine local buffer queue. This queue just pools the outer ByteBuf instance and not
162      * the actual memory and so helps to reduce GC pressure.
163      */
164     private static final int MAGAZINE_BUFFER_QUEUE_CAPACITY = SystemPropertyUtil.getInt(
165             "io.netty.allocator.magazineBufferQueueCapacity", 1024);
166 
167     /**
168      * The size classes are chosen based on the following observation:
169      * <p>
170      * Most allocations, particularly ones above 256 bytes, aim to be a power-of-2. However, many use cases, such
171      * as framing protocols, are themselves operating or moving power-of-2 sized payloads, to which they add a
172      * small amount of overhead, such as headers or checksums.
173      * This means we seem to get a lot of mileage out of having both power-of-2 sizes, and power-of-2-plus-a-bit.
174      * <p>
175      * On the conflicting requirements of both having as few chunks as possible, and having as little wasted
176      * memory within each chunk as possible, this seems to strike a surprisingly good balance for the use cases
177      * tested so far.
178      */
179     private static final int[] SIZE_CLASSES = {
180             32,
181             64,
182             128,
183             256,
184             512,
185             640, // 512 + 128
186             1024,
187             1152, // 1024 + 128
188             2048,
189             2304, // 2048 + 256
190             4096,
191             4352, // 4096 + 256
192             8192,
193             8704, // 8192 + 512
194             16384,
195             16896, // 16384 + 512
196     };
197 
198     private static final int SIZE_CLASSES_COUNT = SIZE_CLASSES.length;
199     private static final byte[] SIZE_INDEXES = new byte[SIZE_CLASSES[SIZE_CLASSES_COUNT - 1] / 32 + 1];
200 
201     static {
202         if (MAGAZINE_BUFFER_QUEUE_CAPACITY < 2) {
203             throw new IllegalArgumentException("MAGAZINE_BUFFER_QUEUE_CAPACITY: " + MAGAZINE_BUFFER_QUEUE_CAPACITY
204                     + " (expected: >= " + 2 + ')');
205         }
206         int lastIndex = 0;
207         for (int i = 0; i < SIZE_CLASSES_COUNT; i++) {
208             int sizeClass = SIZE_CLASSES[i];
209             //noinspection ConstantValue
210             assert (sizeClass & 31) == 0 : "Size class must be a multiple of 32";
211             int sizeIndex = sizeIndexOf(sizeClass);
212             Arrays.fill(SIZE_INDEXES, lastIndex + 1, sizeIndex + 1, (byte) i);
213             lastIndex = sizeIndex;
214         }
215     }
216 
217     private final ChunkAllocator chunkAllocator;
218     private final ChunkRegistry chunkRegistry;
219     private final MagazineGroup[] sizeClassedMagazineGroups;
220     private final MagazineGroup largeBufferMagazineGroup;
221     private final FastThreadLocal<MagazineGroup[]> threadLocalGroup;
222 
223     AdaptivePoolingAllocator(ChunkAllocator chunkAllocator, boolean useCacheForNonEventLoopThreads) {
224         if (PlatformDependent.isJfrEnabled()) {
225             JfrEventRegistration.init();
226         }
227         this.chunkAllocator = ObjectUtil.checkNotNull(chunkAllocator, "chunkAllocator");
228         chunkRegistry = new ChunkRegistry();
229         sizeClassedMagazineGroups = createMagazineGroupSizeClasses(this, false);
230         largeBufferMagazineGroup = new MagazineGroup(
231                 this, chunkAllocator, new BuddyChunkManagementStrategy(), false);
232 
233         boolean disableThreadLocalGroups = IS_LOW_MEM && DISABLE_THREAD_LOCAL_MAGAZINES_ON_LOW_MEM;
234         threadLocalGroup = disableThreadLocalGroups ? null : new FastThreadLocal<MagazineGroup[]>() {
235             @Override
236             protected MagazineGroup[] initialValue() {
237                 if (useCacheForNonEventLoopThreads || ThreadExecutorMap.currentExecutor() != null) {
238                     return createMagazineGroupSizeClasses(AdaptivePoolingAllocator.this, true);
239                 }
240                 return null;
241             }
242 
243             @Override
244             protected void onRemoval(final MagazineGroup[] groups) throws Exception {
245                 if (groups != null) {
246                     for (MagazineGroup group : groups) {
247                         group.free();
248                     }
249                 }
250             }
251         };
252     }
253 
254     private static MagazineGroup[] createMagazineGroupSizeClasses(
255             AdaptivePoolingAllocator allocator, boolean isThreadLocal) {
256         MagazineGroup[] groups = new MagazineGroup[SIZE_CLASSES.length];
257         for (int i = 0; i < SIZE_CLASSES.length; i++) {
258             int segmentSize = SIZE_CLASSES[i];
259             groups[i] = new MagazineGroup(allocator, allocator.chunkAllocator,
260                     new SizeClassChunkManagementStrategy(segmentSize), isThreadLocal);
261         }
262         return groups;
263     }
264 
265     ByteBuf allocate(int size, int maxCapacity) {
266         return allocate(size, maxCapacity, Thread.currentThread(), null);
267     }
268 
269     private AdaptiveByteBuf allocate(int size, int maxCapacity, Thread currentThread, AdaptiveByteBuf buf) {
270         AdaptiveByteBuf allocated = null;
271         if (size <= MAX_POOLED_BUF_SIZE) {
272             final int index = sizeClassIndexOf(size);
273             MagazineGroup[] magazineGroups;
274             if (!FastThreadLocalThread.currentThreadWillCleanupFastThreadLocals() ||
275                     IS_LOW_MEM ||
276                     (magazineGroups = threadLocalGroup.get()) == null) {
277                 magazineGroups = sizeClassedMagazineGroups;
278             }
279             if (index < magazineGroups.length) {
280                 allocated = magazineGroups[index].allocate(size, maxCapacity, currentThread, buf);
281             } else if (!IS_LOW_MEM) {
282                 allocated = largeBufferMagazineGroup.allocate(size, maxCapacity, currentThread, buf);
283             }
284         }
285         if (allocated == null) {
286             allocated = allocateFallback(size, maxCapacity, currentThread, buf);
287         }
288         return allocated;
289     }
290 
291     private static int sizeIndexOf(final int size) {
292         // this is aligning the size to the next multiple of 32 and dividing by 32 to get the size index.
293         return size + 31 >> 5;
294     }
295 
296     static int sizeClassIndexOf(int size) {
297         int sizeIndex = sizeIndexOf(size);
298         if (sizeIndex < SIZE_INDEXES.length) {
299             return SIZE_INDEXES[sizeIndex];
300         }
301         return SIZE_CLASSES_COUNT;
302     }
303 
304     static int[] getSizeClasses() {
305         return SIZE_CLASSES.clone();
306     }
307 
308     private AdaptiveByteBuf allocateFallback(int size, int maxCapacity, Thread currentThread, AdaptiveByteBuf buf) {
309         // If we don't already have a buffer, obtain one from the most conveniently available magazine.
310         Magazine magazine;
311         if (buf != null) {
312             Chunk chunk = buf.chunk;
313             if (chunk == null || chunk == Magazine.MAGAZINE_FREED || (magazine = chunk.currentMagazine()) == null) {
314                 magazine = getFallbackMagazine(currentThread);
315             }
316         } else {
317             magazine = getFallbackMagazine(currentThread);
318             buf = magazine.newBuffer();
319         }
320         // Create a one-off chunk for this allocation.
321         AbstractByteBuf innerChunk = chunkAllocator.allocate(size, maxCapacity);
322         Chunk chunk = new Chunk(innerChunk, magazine, false);
323         chunkRegistry.add(chunk);
324         try {
325             boolean success = chunk.readInitInto(buf, size, size, maxCapacity);
326             assert success : "Failed to initialize ByteBuf with dedicated chunk";
327         } finally {
328             // As the chunk is an one-off we need to always call release explicitly as readInitInto(...)
329             // will take care of retain once when successful. Once The AdaptiveByteBuf is released it will
330             // completely release the Chunk and so the contained innerChunk.
331             chunk.release();
332         }
333         return buf;
334     }
335 
336     private Magazine getFallbackMagazine(Thread currentThread) {
337         Magazine[] mags = largeBufferMagazineGroup.magazines;
338         return mags[(int) currentThread.getId() & mags.length - 1];
339     }
340 
341     /**
342      * Allocate into the given buffer. Used by {@link AdaptiveByteBuf#capacity(int)}.
343      */
344     void reallocate(int size, int maxCapacity, AdaptiveByteBuf into) {
345         AdaptiveByteBuf result = allocate(size, maxCapacity, Thread.currentThread(), into);
346         assert result == into : "Re-allocation created separate buffer instance";
347     }
348 
349     long usedMemory() {
350         return chunkRegistry.totalCapacity();
351     }
352 
353     // Ensure that we release all previous pooled resources when this object is finalized. This is needed as otherwise
354     // we might end up with leaks. While these leaks are usually harmless in reality it would still at least be
355     // very confusing for users.
356     @SuppressWarnings({"FinalizeDeclaration", "deprecation"})
357     @Override
358     protected void finalize() throws Throwable {
359         try {
360             free();
361         } finally {
362             super.finalize();
363         }
364     }
365 
366     private void free() {
367         largeBufferMagazineGroup.free();
368     }
369 
370     private static final class MagazineGroup {
371         private final AdaptivePoolingAllocator allocator;
372         private final ChunkAllocator chunkAllocator;
373         private final ChunkManagementStrategy chunkManagementStrategy;
374         private final ChunkCache chunkCache;
375         private final StampedLock magazineExpandLock;
376         private final Magazine threadLocalMagazine;
377         private Thread ownerThread;
378         private volatile Magazine[] magazines;
379         private volatile boolean freed;
380 
381         MagazineGroup(AdaptivePoolingAllocator allocator,
382                       ChunkAllocator chunkAllocator,
383                       ChunkManagementStrategy chunkManagementStrategy,
384                       boolean isThreadLocal) {
385             this.allocator = allocator;
386             this.chunkAllocator = chunkAllocator;
387             this.chunkManagementStrategy = chunkManagementStrategy;
388             chunkCache = chunkManagementStrategy.createChunkCache(isThreadLocal);
389             if (isThreadLocal) {
390                 ownerThread = Thread.currentThread();
391                 magazineExpandLock = null;
392                 threadLocalMagazine = new Magazine(this, false, chunkManagementStrategy.createController(this));
393             } else {
394                 ownerThread = null;
395                 magazineExpandLock = new StampedLock();
396                 threadLocalMagazine = null;
397                 Magazine[] mags = new Magazine[INITIAL_MAGAZINES];
398                 for (int i = 0; i < mags.length; i++) {
399                     mags[i] = new Magazine(this, true, chunkManagementStrategy.createController(this));
400                 }
401                 magazines = mags;
402             }
403         }
404 
405         public AdaptiveByteBuf allocate(int size, int maxCapacity, Thread currentThread, AdaptiveByteBuf buf) {
406             boolean reallocate = buf != null;
407 
408             // Path for thread-local allocation.
409             Magazine tlMag = threadLocalMagazine;
410             if (tlMag != null) {
411                 if (buf == null) {
412                     buf = tlMag.newBuffer();
413                 }
414                 boolean allocated = tlMag.tryAllocate(size, maxCapacity, buf, reallocate);
415                 assert allocated : "Allocation of threadLocalMagazine must always succeed";
416                 return buf;
417             }
418 
419             // Path for concurrent allocation.
420             long threadId = currentThread.getId();
421             Magazine[] mags;
422             int expansions = 0;
423             do {
424                 mags = magazines;
425                 int mask = mags.length - 1;
426                 int index = (int) (threadId & mask);
427                 for (int i = 0, m = mags.length << 1; i < m; i++) {
428                     Magazine mag = mags[index + i & mask];
429                     if (buf == null) {
430                         buf = mag.newBuffer();
431                     }
432                     if (mag.tryAllocate(size, maxCapacity, buf, reallocate)) {
433                         // Was able to allocate.
434                         return buf;
435                     }
436                 }
437                 expansions++;
438             } while (expansions <= EXPANSION_ATTEMPTS && tryExpandMagazines(mags.length));
439 
440             // The magazines failed us; contention too high and we don't want to spend more effort expanding the array.
441             if (!reallocate && buf != null) {
442                 buf.release(); // Release the previously claimed buffer before we return.
443             }
444             return null;
445         }
446 
447         private boolean tryExpandMagazines(int currentLength) {
448             if (currentLength >= MAX_STRIPES) {
449                 return true;
450             }
451             final Magazine[] mags;
452             long writeLock = magazineExpandLock.tryWriteLock();
453             if (writeLock != 0) {
454                 try {
455                     mags = magazines;
456                     if (mags.length >= MAX_STRIPES || mags.length > currentLength || freed) {
457                         return true;
458                     }
459                     Magazine[] expanded = new Magazine[mags.length * 2];
460                     for (int i = 0, l = expanded.length; i < l; i++) {
461                         expanded[i] = new Magazine(this, true, chunkManagementStrategy.createController(this));
462                     }
463                     magazines = expanded;
464                 } finally {
465                     magazineExpandLock.unlockWrite(writeLock);
466                 }
467                 for (Magazine magazine : mags) {
468                     magazine.free();
469                 }
470             }
471             return true;
472         }
473 
474         Chunk pollChunk(int size) {
475             return chunkCache.pollChunk(size);
476         }
477 
478         boolean offerChunk(Chunk chunk) {
479             if (freed) {
480                 return false;
481             }
482 
483             if (chunk.hasUnprocessedFreelistEntries()) {
484                 chunk.processFreelistEntries();
485             }
486             boolean isAdded = chunkCache.offerChunk(chunk);
487 
488             if (freed && isAdded) {
489                 // Help to free the reuse queue.
490                 freeChunkReuseQueue(ownerThread);
491             }
492             return isAdded;
493         }
494 
495         private void free() {
496             freed = true;
497             Thread ownerThread = this.ownerThread;
498             if (threadLocalMagazine != null) {
499                 this.ownerThread = null;
500                 threadLocalMagazine.free();
501             } else {
502                 long stamp = magazineExpandLock.writeLock();
503                 try {
504                     Magazine[] mags = magazines;
505                     for (Magazine magazine : mags) {
506                         magazine.free();
507                     }
508                 } finally {
509                     magazineExpandLock.unlockWrite(stamp);
510                 }
511             }
512             freeChunkReuseQueue(ownerThread);
513         }
514 
515         private void freeChunkReuseQueue(Thread ownerThread) {
516             if (ownerThread != null && chunkCache instanceof ThreadLocalSizeClassedChunkCache) {
517                 ThreadLocalSizeClassedChunkCache tlCache = (ThreadLocalSizeClassedChunkCache) chunkCache;
518                 int mask = tlCache.chunks.length - 1;
519                 for (int i = 0; i < tlCache.count; i++) {
520                     SizeClassedChunk chunk = tlCache.chunks[(tlCache.head + i) & mask];
521                     assert ownerThread == chunk.ownerThread;
522                     chunk.ownerThread = null;
523                 }
524             }
525             chunkCache.free();
526         }
527     }
528 
529     interface ChunkCache {
530         Chunk pollChunk(int size);
531 
532         boolean offerChunk(Chunk chunk);
533 
534         void free();
535 
536         boolean isEmpty();
537     }
538 
539     // Cached chunks are detached from magazines: no readInitInto can happen, so segment count
540     // can only grow (external releaseSegment returns) and never shrink. Once a chunk reaches
541     // full capacity (hasFullCapacity), it stays idle while in the cache.
542     //
543     // Epoch-based aging invariants (both caches):
544     //
545     // 1. CLASSIFICATION: purge scans all chunks. Idle (hasFullCapacity) → epoch++.
546     //    Non-idle → epoch = 0. Only idle chunks can accumulate epoch.
547     //
548     // 2. EVICTION: idle chunks with epoch > CHUNK_PURGE_THRESHOLD are evicted (markToDeallocate).
549     //    Eviction is immediate — all segments are in, no outstanding references.
550     //    Non-idle chunks are never evicted (deallocation would be deferred, not immediate).
551     //    A retention floor prevents over-eviction: CHUNK_REUSE_QUEUE for the shared cache,
552     //    purgeRetentionFloor (from THREAD_LOCAL_CACHE_MIN_BYTES) for the thread-local cache.
553     //
554     // 3. SCAN RESET: scanForCapacity resets purgeEpoch = 0 on the chunk it picks. The scan
555     //    knows the chunk is being used. The chunk gets allocated from, becomes non-idle, and
556     //    the next purge resets its epoch anyway (non-idle → 0). The scan reset covers the case
557     //    where all segments return before the next purge (short-lived buffers).
558     //
559     // 4. CONVERGENCE: idle chunks that are never picked by scan age undisturbed across
560     //    purge cycles. After CHUNK_PURGE_THRESHOLD + 1 consecutive cycles of being idle and
561     //    unpolled, they are evicted. Chunks picked by scan get epoch reset — aging interrupted.
562     //    Thread-local: partition orders [epoch=0 | 0<epoch<T | epoch>=T | noCap]. Scan takes
563     //    from head (epoch=0 first). Chunks with epoch>=threshold are placed at the back of
564     //    the hasCap zone so scan doesn't reach them — they age to threshold+1 and get evicted.
565     //    Shared: approximate, converges over multiple cycles (FIFO queue ordering,
566     //    LRU preference in scan, retained counter in purge).
567     abstract static class SizeClassedChunkCache implements ChunkCache {
568         static SizeClassedChunkCache create(boolean isThreadLocal, int chunkSize) {
569             return isThreadLocal ? new ThreadLocalSizeClassedChunkCache(chunkSize) :
570                     new SharedSizeClassedChunkCache();
571         }
572 
573         @Override
574         public abstract SizeClassedChunk pollChunk(int size);
575 
576         // Visible for testing: triggers a purge scan bypassing the budget counter.
577         abstract SizeClassedChunk forcePurge();
578     }
579 
580     /**
581      * Ring buffer cache for thread-local chunk reuse (SPSC — only the owner thread accesses it).
582      *
583      * <p>Logical layout after purge:
584      * <pre>
585      *   head                          tail
586      *   v                             v
587      *   [..., notEmpty, notEmpty, ..., empty, empty, ..., null, ...]
588      *        |--- notEmptyCount ---|--- emptyCount --|
589      *        |------------ count ------------------|
590      * </pre>
591      *
592      * <p>Physical layout when the ring wraps:
593      * <pre>
594      *   0         tail          head          length
595      *   v         v             v             v
596      *   [...tail] [  unused  ]  [head................]
597      *             ^             |--- content wraps ---|
598      *             wrap point
599      * </pre>
600      *
601      * <p><b>scanForCapacity</b> — O(1) fast path takes from head while {@code notEmptyCount > 0}:
602      * <pre>
603      *   before: notEmptyCount=2, count=5
604      *   [NE, NE, E, E, E, _, _, _]
605      *    ^head            ^tail
606      *
607      *   after: returns NE, notEmptyCount=1, count=4
608      *   [_,  NE, E, E, E, _, _, _]
609      *        ^head        ^tail
610      * </pre>
611      * Fallback when {@code notEmptyCount == 0}: linear scan of the empty zone for chunks
612      * that gained capacity from external segment returns.
613      *
614      * <p><b>offerChunk</b> — write at tail, grow (double + linearize) if full:
615      * <pre>
616      *   before: count=4
617      *   [_,  NE, E, E, E, _, _, _]
618      *        ^head        ^tail
619      *
620      *   after: count=5
621      *   [_,  NE, E, E, E, X, _, _]
622      *        ^head           ^tail
623      * </pre>
624      *
625      * <p><b>runPurgeScan</b> (every {@link #CHUNK_PURGE_POLLS_THREAD_LOCAL} polls) —
626      * two passes. Pass 1: age idle chunks (full → epoch++, non-full → epoch=0), evict
627      * past threshold, compact survivors (nulls stale slots inline). Pass 2: partition
628      * hasCap to front / noCap to back, then three-way Dutch-flag within hasCap into
629      * [epoch=0 | 0&lt;epoch&lt;threshold | epoch&gt;=threshold]. Chunks with epoch&gt;=threshold
630      * are placed at the back of hasCap so scan doesn't reach them — they age to
631      * threshold+1 and get evicted. Never selects — selection is always
632      * {@code scanForCapacity}.
633      *
634      * <p>Case 1 — no eviction, an empty chunk gained capacity externally (common):
635      * <pre>
636      *   before (E* gained capacity since last purge):
637      *   [NE, NE, E*, E, _, _, _, _]
638      *    ^head            ^tail
639      *    notEmptyCount=2
640      *
641      *   pass 1: age idle chunks. None past threshold. No compaction needed.
642      *   pass 2 (partition): E* now has capacity → placed in notEmpty zone.
643      *
644      *   after:
645      *   [NE, NE, E*, E, _, _, _, _]
646      *    ^head            ^tail
647      *    notEmptyCount=3
648      * </pre>
649      *
650      * <p>Case 2 — eviction (uncommon, burst wind-down):
651      * <pre>
652      *   before (ring wraps, IDLE* = idle past threshold):
653      *   [E, NE, _,  IDLE*, NE, E, E, NE]
654      *          ^tail ^head
655      *
656      *   pass 1: IDLE* evicted (markToDeallocate), survivors compacted, stale slots nulled.
657      *   [_, _, _,  NE, E, E, NE, E]
658      *     ^tail    ^head
659      *              |--- kept=6 ---|
660      *
661      *   pass 2 (partition): [epoch=0 hasCap | 0&lt;epoch&lt;T hasCap | epoch&gt;=T hasCap | noCap].
662      *   [_, _, _,  NE, NE, E, E, E]
663      *     ^tail    ^head
664      *              notEmptyCount=2, count=6
665      * </pre>
666      * Idle chunks ({@code remainingCapacity == capacity}) age via purgeEpoch and are evicted
667      * past threshold, but at least {@code purgeRetentionFloor} chunks are always retained.
668      */
669     static final class ThreadLocalSizeClassedChunkCache extends SizeClassedChunkCache {
670         SizeClassedChunk[] chunks; // package-private for testing
671         int head;
672         int tail;
673         int count;
674         int notEmptyCount;
675         private long purgeBudget;
676         final int maxCachedChunks; // package-private for testing
677         final int purgeRetentionFloor; // package-private for testing
678 
679         ThreadLocalSizeClassedChunkCache(int chunkSize) {
680             chunks = new SizeClassedChunk[8];
681             purgeBudget = CHUNK_PURGE_POLLS_THREAD_LOCAL;
682             maxCachedChunks = Math.max(1, THREAD_LOCAL_CACHE_MAX_BYTES / chunkSize);
683             purgeRetentionFloor = Math.min(maxCachedChunks,
684                     Math.max(1, THREAD_LOCAL_CACHE_MIN_BYTES / chunkSize));
685         }
686 
687         @Override
688         SizeClassedChunk forcePurge() {
689             purgeBudget = 1;
690             return pollChunk(0);
691         }
692 
693         @Override
694         public SizeClassedChunk pollChunk(int size) {
695             if (--purgeBudget == 0) {
696                 runPurgeScan();
697             }
698             return scanForCapacity();
699         }
700 
701         private SizeClassedChunk scanForCapacity() {
702             if (notEmptyCount > 0) {
703                 SizeClassedChunk chunk = chunks[head];
704                 assert chunk.hasRemainingCapacity();
705                 chunk.purgeEpoch = 0;
706                 chunks[head] = null;
707                 head = (head + 1) & (chunks.length - 1);
708                 count--;
709                 notEmptyCount--;
710                 return chunk;
711             }
712             return scanForCapacityFallback();
713         }
714 
715         private SizeClassedChunk scanForCapacityFallback() {
716             int mask = chunks.length - 1;
717             int emptyCount = count - notEmptyCount;
718             int pos = (head + notEmptyCount) & mask;
719             for (int i = 0; i < emptyCount; i++) {
720                 SizeClassedChunk chunk = chunks[pos];
721                 if (chunk.hasRemainingCapacity()) {
722                     chunk.purgeEpoch = 0;
723                     int lastIdx = (tail - 1) & mask;
724                     chunks[pos] = chunks[lastIdx];
725                     chunks[lastIdx] = null;
726                     tail = lastIdx;
727                     count--;
728                     return chunk;
729                 }
730                 pos = (pos + 1) & mask;
731             }
732             return null;
733         }
734 
735         private void runPurgeScan() {
736             int mask = chunks.length - 1;
737             int kept = 0;
738             int survivors = count;
739             for (int i = 0; i < count; i++) {
740                 int readIdx = (head + i) & mask;
741                 SizeClassedChunk chunk = chunks[readIdx];
742                 if (chunk.purgeEpoch > 0) {
743                     assert chunk.hasFullCapacity();
744                     chunk.purgeEpoch++;
745                     if (chunk.purgeEpoch > CHUNK_PURGE_THRESHOLD && survivors > purgeRetentionFloor) {
746                         chunk.markToDeallocate();
747                         chunks[readIdx] = null;
748                         survivors--;
749                         continue;
750                     }
751                 } else if (chunk.hasFullCapacity()) {
752                     chunk.purgeEpoch = 1;
753                 }
754                 int writeIdx = (head + kept) & mask;
755                 if (writeIdx != readIdx) {
756                     chunks[writeIdx] = chunk;
757                     chunks[readIdx] = null;
758                 }
759                 kept++;
760             }
761             tail = (head + kept) & mask;
762             count = kept;
763             partition(kept);
764             purgeBudget = CHUNK_PURGE_POLLS_THREAD_LOCAL;
765         }
766 
767         private void partition(int size) {
768             int mask = chunks.length - 1;
769             // Pass 1: hasCapacity to front, noCapacity to back.
770             int lo = 0;
771             int hi = size - 1;
772             while (lo <= hi) {
773                 int loIdx = (head + lo) & mask;
774                 if (chunks[loIdx].hasRemainingCapacity()) {
775                     lo++;
776                 } else {
777                     int hiIdx = (head + hi) & mask;
778                     SizeClassedChunk tmp = chunks[loIdx];
779                     chunks[loIdx] = chunks[hiIdx];
780                     chunks[hiIdx] = tmp;
781                     hi--;
782                 }
783             }
784             notEmptyCount = lo;
785             // Pass 2: three-way Dutch-flag within notEmpty:
786             //   [epoch=0 | 0<epoch<threshold | epoch>=threshold]
787             //
788             // Epoch=0 (recently used) at head — scan picks these first.
789             // Epoch>=threshold (about to be evicted) at back — scan doesn't reach them,
790             // so they age one more cycle to threshold+1 and get evicted.
791             //
792             // This ordering guarantees convergence regardless of count/polls ratio.
793             // Without it (e.g., a simple epoch=0/epoch>0 split with mid++), when
794             // count/polls == threshold the groups rotate perfectly and max epoch never
795             // exceeds threshold — eviction stalls at threshold * polls chunks.
796             int elo = 0;
797             int emid = 0;
798             int ehi = lo - 1;
799             while (emid <= ehi) {
800                 int emidIdx = (head + emid) & mask;
801                 SizeClassedChunk c = chunks[emidIdx];
802                 if (c.purgeEpoch == 0) {
803                     if (elo != emid) {
804                         int eloIdx = (head + elo) & mask;
805                         chunks[emidIdx] = chunks[eloIdx];
806                         chunks[eloIdx] = c;
807                     }
808                     elo++;
809                     emid++;
810                 } else if (c.purgeEpoch < CHUNK_PURGE_THRESHOLD) {
811                     emid++;
812                 } else {
813                     int ehiIdx = (head + ehi) & mask;
814                     chunks[emidIdx] = chunks[ehiIdx];
815                     chunks[ehiIdx] = c;
816                     ehi--;
817                 }
818             }
819         }
820 
821         @Override
822         public boolean offerChunk(Chunk chunk) {
823             if (count >= maxCachedChunks) {
824                 return false;
825             }
826             if (count == chunks.length) {
827                 SizeClassedChunk[] newChunks = new SizeClassedChunk[chunks.length * 2];
828                 for (int i = 0; i < count; i++) {
829                     newChunks[i] = chunks[(head + i) & (chunks.length - 1)];
830                 }
831                 chunks = newChunks;
832                 head = 0;
833                 tail = count;
834             }
835             chunks[tail] = (SizeClassedChunk) chunk;
836             tail = (tail + 1) & (chunks.length - 1);
837             count++;
838             return true;
839         }
840 
841         @Override
842         public String toString() {
843             int mask = chunks.length - 1;
844             StringBuilder sb = new StringBuilder();
845             sb.append("ThreadLocalCache[head=").append(head)
846                     .append(", tail=").append(tail)
847                     .append(", count=").append(count)
848                     .append(", notEmpty=").append(notEmptyCount)
849                     .append(", length=").append(chunks.length)
850                     .append("]\n  ");
851             for (int i = 0; i < count; i++) {
852                 if (i > 0) {
853                     sb.append(", ");
854                 }
855                 if (i == notEmptyCount) {
856                     sb.append("| ");
857                 }
858                 SizeClassedChunk c = chunks[(head + i) & mask];
859                 String region = i < notEmptyCount ? "notEmpty" : "empty";
860                 String actual = c == null ? "null" :
861                         c.hasRemainingCapacity() ? "hasCap" : "noCap";
862                 sb.append('[').append(region).append(':').append(actual)
863                         .append(",ep=").append(c == null ? -1 : c.purgeEpoch).append(']');
864             }
865             return sb.toString();
866         }
867 
868         @Override
869         public void free() {
870             int mask = chunks.length - 1;
871             for (int i = 0; i < count; i++) {
872                 int idx = (head + i) & mask;
873                 chunks[idx].markToDeallocate();
874                 chunks[idx] = null;
875             }
876             head = 0;
877             tail = 0;
878             count = 0;
879             notEmptyCount = 0;
880         }
881 
882         @Override
883         public boolean isEmpty() {
884             return count == 0;
885         }
886     }
887 
888     /**
889      * MPMC queue cache for shared (cross-thread) chunk reuse.
890      *
891      * <p><b>scanForCapacity</b> — LRU preference with fallback:
892      * <pre>
893      *   fast path: head chunk has purgeEpoch == 0 and capacity → return O(1)
894      *
895      *   slow path: scan for epoch=0 chunk, hold first idle (epoch &gt; 0) as fallback
896      *     queue: [E&gt;0, E&gt;0, E=0, E&gt;0, ...]
897      *             skip   skip  ↑ return (put fallback back)
898      *
899      *   no epoch=0 found → use fallback, reset its epoch to 0
900      * </pre>
901      *
902      * <p>The LRU preference creates a natural separation: recently-used chunks (epoch=0,
903      * returned via {@link #offerChunk} after magazine use) cycle at the front. Idle chunks
904      * (epoch &gt; 0, aged by purge) are scanned past but never returned — they age undisturbed.
905      * When no recently-used chunks exist, idle ones are reused (fallback) rather than
906      * allocating new chunks.
907      *
908      * <p>All re-offered chunks are stamped with {@code lastScanGeneration} for cycle detection.
909      * The {@code >=} check terminates the scan when encountering any chunk already processed
910      * by this or a later scan, preventing livelock under concurrent access.
911      *
912      * <p><b>runPurgeScan</b> (every {@link #CHUNK_PURGE_POLLS_SHARED} polls):
913      * drains the queue, ages full chunks (epoch++), resets non-full (epoch=0).
914      * Non-candidate capacity chunks are re-offered inline. Eviction candidates (full,
915      * epoch past threshold) and no-capacity chunks are deferred to a buffer. After the drain,
916      * the buffer is walked with the known total: candidates are evicted while above
917      * {@link #CHUNK_REUSE_QUEUE}, remainder re-offered. No selection — that is
918      * {@code scanForCapacity}'s job (called after purge via {@code pollChunk}).
919      */
920     static final class SharedSizeClassedChunkCache extends SizeClassedChunkCache {
921         // Must exceed CHUNK_REUSE_QUEUE (the retention floor) to leave room for burst absorption.
922         // TODO replace with an unbounded concurrent collection once available.
923         private static final int SHARED_CACHE_CAPACITY = Math.max(16, CHUNK_REUSE_QUEUE * 2);
924         private final Queue<SizeClassedChunk> queue;
925         private final AtomicLong purgeBudget;
926         private final ArrayList<SizeClassedChunk> deferredBuffer = new ArrayList<>();
927         private long purgeGeneration;
928         private final AtomicLong scanGeneration = new AtomicLong();
929 
930         SharedSizeClassedChunkCache() {
931             queue = PlatformDependent.newFixedMpmcQueue(SHARED_CACHE_CAPACITY);
932             purgeBudget = new AtomicLong(CHUNK_PURGE_POLLS_SHARED);
933         }
934 
935         @Override
936         SizeClassedChunk forcePurge() {
937             purgeBudget.set(1);
938             return pollChunk(0);
939         }
940 
941         @Override
942         public SizeClassedChunk pollChunk(int size) {
943             long budget = purgeBudget.decrementAndGet();
944             if (budget == 0) {
945                 runPurgeScan();
946             }
947             return scanForCapacity();
948         }
949 
950         private SizeClassedChunk scanForCapacity() {
951             SizeClassedChunk first = queue.poll();
952             if (first == null) {
953                 return null;
954             }
955             if (first.purgeEpoch == 0 && first.hasRemainingCapacity()) {
956                 return first;
957             }
958             long generation = scanGeneration.incrementAndGet();
959             first.lastScanGeneration = generation;
960             if (first.hasRemainingCapacity()) {
961                 return scanForCapacitySlow(generation, first);
962             }
963             offerOrDeallocate(first);
964             return scanForCapacitySlow(generation, null);
965         }
966 
967         private SizeClassedChunk scanForCapacitySlow(long generation, SizeClassedChunk fallback) {
968             SizeClassedChunk chunk;
969             while ((chunk = queue.poll()) != null) {
970                 if (chunk.lastScanGeneration >= generation) {
971                     offerOrDeallocate(chunk);
972                     break;
973                 }
974                 if (chunk.hasRemainingCapacity()) {
975                     if (chunk.purgeEpoch == 0) {
976                         if (fallback != null) {
977                             offerOrDeallocate(fallback);
978                         }
979                         return chunk;
980                     }
981                     if (fallback == null) {
982                         fallback = chunk;
983                         continue;
984                     }
985                 }
986                 chunk.lastScanGeneration = generation;
987                 offerOrDeallocate(chunk);
988             }
989             if (fallback != null) {
990                 fallback.purgeEpoch = 0;
991                 return fallback;
992             }
993             return null;
994         }
995 
996         private boolean offerOrDeallocate(SizeClassedChunk chunk) {
997             if (!queue.offer(chunk)) {
998                 chunk.markToDeallocate();
999                 return false;
1000             }
1001             return true;
1002         }
1003 
1004         private boolean offerOrDeallocate(SizeClassedChunk chunk, long generation) {
1005             chunk.lastPurgeGeneration = generation;
1006             return offerOrDeallocate(chunk);
1007         }
1008 
1009         private void runPurgeScan() {
1010             long generation = ++purgeGeneration;
1011             int retained = 0;
1012             ArrayList<SizeClassedChunk> deferred = deferredBuffer;
1013             SizeClassedChunk chunk;
1014             while ((chunk = queue.poll()) != null) {
1015                 if (chunk.lastPurgeGeneration == generation) {
1016                     offerOrDeallocate(chunk, generation);
1017                     break;
1018                 }
1019                 retained++;
1020                 if (chunk.hasFullCapacity()) {
1021                     chunk.purgeEpoch++;
1022                     if (chunk.purgeEpoch > CHUNK_PURGE_THRESHOLD) {
1023                         deferred.add(chunk);
1024                         continue;
1025                     }
1026                 } else {
1027                     chunk.purgeEpoch = 0;
1028                 }
1029                 int remaining = chunk.remainingCapacity();
1030                 if (remaining > 0) {
1031                     if (!offerOrDeallocate(chunk, generation)) {
1032                         retained--;
1033                     }
1034                 } else {
1035                     deferred.add(chunk);
1036                 }
1037             }
1038             for (int i = 0, size = deferred.size(); i < size; i++) {
1039                 chunk = deferred.get(i);
1040                 if (chunk.purgeEpoch > CHUNK_PURGE_THRESHOLD && retained > CHUNK_REUSE_QUEUE) {
1041                     chunk.markToDeallocate();
1042                     retained--;
1043                 } else {
1044                     if (!offerOrDeallocate(chunk, generation)) {
1045                         retained--;
1046                     }
1047                 }
1048             }
1049             deferred.clear();
1050             purgeBudget.lazySet(CHUNK_PURGE_POLLS_SHARED);
1051         }
1052 
1053         @Override
1054         public boolean offerChunk(Chunk chunk) {
1055             return queue.offer((SizeClassedChunk) chunk);
1056         }
1057 
1058         @Override
1059         public void free() {
1060             SizeClassedChunk chunk;
1061             while ((chunk = queue.poll()) != null) {
1062                 chunk.markToDeallocate();
1063             }
1064         }
1065 
1066         @Override
1067         public boolean isEmpty() {
1068             return queue.isEmpty();
1069         }
1070     }
1071 
1072     private static final class ConcurrentSkipListChunkCache implements ChunkCache {
1073         private final ConcurrentSkipListIntObjMultimap<Chunk> chunks;
1074 
1075         private ConcurrentSkipListChunkCache() {
1076             chunks = new ConcurrentSkipListIntObjMultimap<>(-1);
1077         }
1078 
1079         @Override
1080         public Chunk pollChunk(int size) {
1081             if (chunks.isEmpty()) {
1082                 return null;
1083             }
1084             IntEntry<Chunk> entry = chunks.pollCeilingEntry(size);
1085             if (entry != null) {
1086                 Chunk chunk = entry.getValue();
1087                 if (chunk.hasUnprocessedFreelistEntries()) {
1088                     chunk.processFreelistEntries();
1089                 }
1090                 return chunk;
1091             }
1092 
1093             Chunk bestChunk = null;
1094             int bestRemainingCapacity = 0;
1095             Iterator<IntEntry<Chunk>> itr = chunks.iterator();
1096             while (itr.hasNext()) {
1097                 entry = itr.next();
1098                 final Chunk chunk;
1099                 if (entry != null && (chunk = entry.getValue()).hasUnprocessedFreelistEntries()) {
1100                     if (!chunks.remove(entry.getKey(), entry.getValue())) {
1101                         continue;
1102                     }
1103                     chunk.processFreelistEntries();
1104                     int remainingCapacity = chunk.remainingCapacity();
1105                     if (remainingCapacity >= size &&
1106                             (bestChunk == null || remainingCapacity > bestRemainingCapacity)) {
1107                         if (bestChunk != null) {
1108                             chunks.put(bestRemainingCapacity, bestChunk);
1109                         }
1110                         bestChunk = chunk;
1111                         bestRemainingCapacity = remainingCapacity;
1112                     } else {
1113                         chunks.put(remainingCapacity, chunk);
1114                     }
1115                 }
1116             }
1117 
1118             return bestChunk;
1119         }
1120 
1121         @Override
1122         public boolean offerChunk(Chunk chunk) {
1123             chunks.put(chunk.remainingCapacity(), chunk);
1124 
1125             int size = chunks.size();
1126             while (size > CHUNK_REUSE_QUEUE) {
1127                 int key = -1;
1128                 Chunk toDeallocate = null;
1129                 for (IntEntry<Chunk> entry : chunks) {
1130                     Chunk candidate = entry.getValue();
1131                     if (candidate != null && RefCnt.refCnt(candidate.refCnt) == 1) {
1132                         toDeallocate = candidate;
1133                         key = entry.getKey();
1134                         break;
1135                     }
1136                 }
1137                 if (toDeallocate == null) {
1138                     break;
1139                 }
1140                 if (chunks.remove(key, toDeallocate)) {
1141                     toDeallocate.markToDeallocate();
1142                 }
1143                 size = chunks.size();
1144             }
1145             return true;
1146         }
1147 
1148         @Override
1149         public void free() {
1150             for (IntEntry<Chunk> entry : chunks) {
1151                 Chunk chunk = entry.getValue();
1152                 if (chunk != null && chunks.remove(entry.getKey(), chunk)) {
1153                     chunk.markToDeallocate();
1154                 }
1155             }
1156         }
1157 
1158         @Override
1159         public boolean isEmpty() {
1160             return chunks.isEmpty();
1161         }
1162     }
1163 
1164     private interface ChunkManagementStrategy {
1165         ChunkController createController(MagazineGroup group);
1166 
1167         ChunkCache createChunkCache(boolean isThreadLocal);
1168     }
1169 
1170     private interface ChunkController {
1171         /**
1172          * Compute the "fast max capacity" value for the buffer.
1173          */
1174         int computeBufferCapacity(int requestedSize, int maxCapacity, boolean isReallocation);
1175 
1176         /**
1177          * Allocate a new {@link Chunk} for the given {@link Magazine}.
1178          */
1179         Chunk newChunkAllocation(int promptingSize, Magazine magazine);
1180     }
1181 
1182     private static final class SizeClassChunkManagementStrategy implements ChunkManagementStrategy {
1183         // To amortize activation/deactivation of chunks, we should have a minimum number of segments per chunk.
1184         // We choose 32 because it seems neither too small nor too big.
1185         // For segments of 16 KiB, the chunks will be half a megabyte.
1186         private static final int MIN_SEGMENTS_PER_CHUNK = 32;
1187         private final int segmentSize;
1188         private final int chunkSize;
1189 
1190         private SizeClassChunkManagementStrategy(int segmentSize) {
1191             this.segmentSize = ObjectUtil.checkPositive(segmentSize, "segmentSize");
1192             chunkSize = Math.max(MIN_CHUNK_SIZE, segmentSize * MIN_SEGMENTS_PER_CHUNK);
1193         }
1194 
1195         @Override
1196         public ChunkController createController(MagazineGroup group) {
1197             return new SizeClassChunkController(group, segmentSize, chunkSize);
1198         }
1199 
1200         @Override
1201         public ChunkCache createChunkCache(boolean isThreadLocal) {
1202             return SizeClassedChunkCache.create(isThreadLocal, chunkSize);
1203         }
1204     }
1205 
1206     private static final class SizeClassChunkController implements ChunkController {
1207 
1208         private final ChunkAllocator chunkAllocator;
1209         private final int segmentSize;
1210         private final int chunkSize;
1211         private final ChunkRegistry chunkRegistry;
1212 
1213         private SizeClassChunkController(MagazineGroup group, int segmentSize, int chunkSize) {
1214             chunkAllocator = group.chunkAllocator;
1215             this.segmentSize = segmentSize;
1216             this.chunkSize = chunkSize;
1217             chunkRegistry = group.allocator.chunkRegistry;
1218         }
1219 
1220         private MpscIntQueue createEmptyFreeList() {
1221             return MpscIntQueue.create(chunkSize / segmentSize, SizeClassedChunk.FREE_LIST_EMPTY);
1222         }
1223 
1224         private MpscIntQueue createFreeList() {
1225             final int segmentsCount = chunkSize / segmentSize;
1226             final MpscIntQueue freeList = MpscIntQueue.create(segmentsCount, SizeClassedChunk.FREE_LIST_EMPTY);
1227             int segmentOffset = 0;
1228             for (int i = 0; i < segmentsCount; i++) {
1229                 freeList.offer(segmentOffset);
1230                 segmentOffset += segmentSize;
1231             }
1232             return freeList;
1233         }
1234 
1235         private IntStack createLocalFreeList() {
1236             final int segmentsCount = chunkSize / segmentSize;
1237             int segmentOffset = chunkSize;
1238             int[] offsets = new int[segmentsCount];
1239             for (int i = 0; i < segmentsCount; i++) {
1240                 segmentOffset -= segmentSize;
1241                 offsets[i] = segmentOffset;
1242             }
1243             return new IntStack(offsets);
1244         }
1245 
1246         @Override
1247         public int computeBufferCapacity(
1248                 int requestedSize, int maxCapacity, boolean isReallocation) {
1249             return Math.min(segmentSize, maxCapacity);
1250         }
1251 
1252         @Override
1253         public Chunk newChunkAllocation(int promptingSize, Magazine magazine) {
1254             AbstractByteBuf chunkBuffer = chunkAllocator.allocate(chunkSize, chunkSize);
1255             assert chunkBuffer.capacity() == chunkSize;
1256             SizeClassedChunk chunk = new SizeClassedChunk(chunkBuffer, magazine, this);
1257             chunkRegistry.add(chunk);
1258             return chunk;
1259         }
1260     }
1261 
1262     private static final class BuddyChunkManagementStrategy implements ChunkManagementStrategy {
1263         private final AtomicInteger maxChunkSize = new AtomicInteger();
1264 
1265         @Override
1266         public ChunkController createController(MagazineGroup group) {
1267             return new BuddyChunkController(group, maxChunkSize);
1268         }
1269 
1270         @Override
1271         public ChunkCache createChunkCache(boolean isThreadLocal) {
1272             return new ConcurrentSkipListChunkCache();
1273         }
1274     }
1275 
1276     private static final class BuddyChunkController implements ChunkController {
1277         private final ChunkAllocator chunkAllocator;
1278         private final ChunkRegistry chunkRegistry;
1279         private final AtomicInteger maxChunkSize;
1280 
1281         BuddyChunkController(MagazineGroup group, AtomicInteger maxChunkSize) {
1282             chunkAllocator = group.chunkAllocator;
1283             chunkRegistry = group.allocator.chunkRegistry;
1284             this.maxChunkSize = maxChunkSize;
1285         }
1286 
1287         @Override
1288         public int computeBufferCapacity(int requestedSize, int maxCapacity, boolean isReallocation) {
1289             return MathUtil.safeFindNextPositivePowerOfTwo(requestedSize);
1290         }
1291 
1292         @Override
1293         public Chunk newChunkAllocation(int promptingSize, Magazine magazine) {
1294             int maxChunkSize = this.maxChunkSize.get();
1295             int proposedChunkSize = MathUtil.safeFindNextPositivePowerOfTwo(BUFS_PER_CHUNK * promptingSize);
1296             int chunkSize = Math.min(MAX_CHUNK_SIZE, Math.max(maxChunkSize, proposedChunkSize));
1297             if (chunkSize > maxChunkSize) {
1298                 // Update our stored max chunk size. It's fine that this is racy.
1299                 this.maxChunkSize.set(chunkSize);
1300             }
1301             BuddyChunk chunk = new BuddyChunk(chunkAllocator.allocate(chunkSize, chunkSize), magazine);
1302             chunkRegistry.add(chunk);
1303             return chunk;
1304         }
1305     }
1306 
1307     private static final class Magazine {
1308         private static final AtomicReferenceFieldUpdater<Magazine, Chunk> NEXT_IN_LINE;
1309 
1310         static {
1311             NEXT_IN_LINE = AtomicReferenceFieldUpdater.newUpdater(Magazine.class, Chunk.class, "nextInLine");
1312         }
1313 
1314         private static final Chunk MAGAZINE_FREED = new Chunk();
1315 
1316         private static final class AdaptiveRecycler extends Recycler<AdaptiveByteBuf> {
1317 
1318             private AdaptiveRecycler(boolean unguarded) {
1319                 // uses fast thread local
1320                 super(unguarded);
1321             }
1322 
1323             private AdaptiveRecycler(int maxCapacity, boolean unguarded) {
1324                 // doesn't use fast thread local, shared
1325                 super(maxCapacity, unguarded);
1326             }
1327 
1328             @Override
1329             protected AdaptiveByteBuf newObject(final Handle<AdaptiveByteBuf> handle) {
1330                 return new AdaptiveByteBuf((EnhancedHandle<AdaptiveByteBuf>) handle);
1331             }
1332 
1333             public static AdaptiveRecycler threadLocal() {
1334                 return new AdaptiveRecycler(true);
1335             }
1336 
1337             public static AdaptiveRecycler sharedWith(int maxCapacity) {
1338                 return new AdaptiveRecycler(maxCapacity, true);
1339             }
1340         }
1341 
1342         private static final AdaptiveRecycler EVENT_LOOP_LOCAL_BUFFER_POOL = AdaptiveRecycler.threadLocal();
1343 
1344         private Chunk current;
1345         @SuppressWarnings("unused") // updated via NEXT_IN_LINE
1346         private volatile Chunk nextInLine;
1347         private final MagazineGroup group;
1348         private final ChunkController chunkController;
1349         private final StampedLock allocationLock;
1350         private final AdaptiveRecycler recycler;
1351 
1352         Magazine(MagazineGroup group, boolean shareable, ChunkController chunkController) {
1353             this.group = group;
1354             this.chunkController = chunkController;
1355 
1356             if (shareable) {
1357                 // We only need the StampedLock if this Magazine will be shared across threads.
1358                 allocationLock = new StampedLock();
1359                 recycler = AdaptiveRecycler.sharedWith(MAGAZINE_BUFFER_QUEUE_CAPACITY);
1360             } else {
1361                 allocationLock = null;
1362                 recycler = null;
1363             }
1364         }
1365 
1366         public boolean tryAllocate(int size, int maxCapacity, AdaptiveByteBuf buf, boolean reallocate) {
1367             if (allocationLock == null) {
1368                 // This magazine is not shared across threads, just allocate directly.
1369                 return allocate(size, maxCapacity, buf, reallocate);
1370             }
1371 
1372             // Try to retrieve the lock and if successful allocate.
1373             long writeLock = allocationLock.tryWriteLock();
1374             if (writeLock != 0) {
1375                 try {
1376                     return allocate(size, maxCapacity, buf, reallocate);
1377                 } finally {
1378                     allocationLock.unlockWrite(writeLock);
1379                 }
1380             }
1381             return allocateWithoutLock(size, maxCapacity, buf);
1382         }
1383 
1384         private boolean allocateWithoutLock(int size, int maxCapacity, AdaptiveByteBuf buf) {
1385             Chunk curr = NEXT_IN_LINE.getAndSet(this, null);
1386             if (curr == MAGAZINE_FREED) {
1387                 // Allocation raced with a stripe-resize that freed this magazine.
1388                 restoreMagazineFreed();
1389                 return false;
1390             }
1391             if (curr == null) {
1392                 curr = group.pollChunk(size);
1393                 if (curr == null) {
1394                     return false;
1395                 }
1396                 curr.attachToMagazine(this);
1397             }
1398             boolean allocated = false;
1399             int remainingCapacity = curr.remainingCapacity();
1400             int startingCapacity = chunkController.computeBufferCapacity(
1401                     size, maxCapacity, true /* never update stats as we don't hold the magazine lock */);
1402             if (remainingCapacity >= size &&
1403                     curr.readInitInto(buf, size, Math.min(remainingCapacity, startingCapacity), maxCapacity)) {
1404                 allocated = true;
1405                 remainingCapacity = curr.remainingCapacity();
1406             }
1407             try {
1408                 if (remainingCapacity >= RETIRE_CAPACITY) {
1409                     transferToNextInLineOrRelease(curr);
1410                     curr = null;
1411                 }
1412             } finally {
1413                 if (curr != null) {
1414                     curr.releaseFromMagazine();
1415                 }
1416             }
1417             return allocated;
1418         }
1419 
1420         private boolean allocate(int size, int maxCapacity, AdaptiveByteBuf buf, boolean reallocate) {
1421             int startingCapacity = chunkController.computeBufferCapacity(size, maxCapacity, reallocate);
1422             Chunk curr = current;
1423             if (curr != null) {
1424                 boolean success = curr.readInitInto(buf, size, startingCapacity, maxCapacity);
1425                 int remainingCapacity = curr.remainingCapacity();
1426                 if (!success && remainingCapacity > 0) {
1427                     current = null;
1428                     transferToNextInLineOrRelease(curr);
1429                 } else if (remainingCapacity == 0) {
1430                     current = null;
1431                     curr.releaseFromMagazine();
1432                 }
1433                 if (success) {
1434                     return true;
1435                 }
1436             }
1437 
1438             assert current == null;
1439             // The fast-path for allocations did not work.
1440             //
1441             // Try to fetch the next "Magazine local" Chunk first, if this fails because we don't have a
1442             // next-in-line chunk available, we will poll our centralQueue.
1443             // If this fails as well we will just allocate a new Chunk.
1444             //
1445             // In any case we will store the Chunk as the current so it will be used again for the next allocation and
1446             // thus be "reserved" by this Magazine for exclusive usage.
1447             curr = NEXT_IN_LINE.getAndSet(this, null);
1448             if (curr != null) {
1449                 if (curr == MAGAZINE_FREED) {
1450                     // Allocation raced with a stripe-resize that freed this magazine.
1451                     restoreMagazineFreed();
1452                     return false;
1453                 }
1454 
1455                 int remainingCapacity = curr.remainingCapacity();
1456                 if (remainingCapacity > startingCapacity &&
1457                         curr.readInitInto(buf, size, startingCapacity, maxCapacity)) {
1458                     // We have a Chunk that has some space left.
1459                     current = curr;
1460                     return true;
1461                 }
1462 
1463                 try {
1464                     if (remainingCapacity >= size) {
1465                         // At this point we know that this will be the last time curr will be used, so directly set it
1466                         // to null and release it once we are done.
1467                         return curr.readInitInto(buf, size, remainingCapacity, maxCapacity);
1468                     }
1469                 } finally {
1470                     // Release in a finally block so even if readInitInto(...) would throw we would still correctly
1471                     // release the current chunk before null it out.
1472                     curr.releaseFromMagazine();
1473                 }
1474             }
1475 
1476             // Now try to poll from the central queue first
1477             curr = group.pollChunk(size);
1478             if (curr == null) {
1479                 curr = chunkController.newChunkAllocation(size, this);
1480             } else {
1481                 curr.attachToMagazine(this);
1482 
1483                 int remainingCapacity = curr.remainingCapacity();
1484                 if (remainingCapacity == 0 || remainingCapacity < size) {
1485                     // Check if we either retain the chunk in the nextInLine cache or releasing it.
1486                     if (remainingCapacity < RETIRE_CAPACITY) {
1487                         curr.releaseFromMagazine();
1488                     } else {
1489                         // See if it makes sense to transfer the Chunk to the nextInLine cache for later usage.
1490                         // This method will release curr if this is not the case
1491                         transferToNextInLineOrRelease(curr);
1492                     }
1493                     curr = chunkController.newChunkAllocation(size, this);
1494                 }
1495             }
1496 
1497             current = curr;
1498             boolean success;
1499             try {
1500                 int remainingCapacity = curr.remainingCapacity();
1501                 assert remainingCapacity >= size;
1502                 if (remainingCapacity > startingCapacity) {
1503                     success = curr.readInitInto(buf, size, startingCapacity, maxCapacity);
1504                     curr = null;
1505                 } else {
1506                     success = curr.readInitInto(buf, size, remainingCapacity, maxCapacity);
1507                 }
1508             } finally {
1509                 if (curr != null) {
1510                     // Release in a finally block so even if readInitInto(...) would throw we would still correctly
1511                     // release the current chunk before null it out.
1512                     curr.releaseFromMagazine();
1513                     current = null;
1514                 }
1515             }
1516             return success;
1517         }
1518 
1519         private void restoreMagazineFreed() {
1520             Chunk next = NEXT_IN_LINE.getAndSet(this, MAGAZINE_FREED);
1521             if (next != null && next != MAGAZINE_FREED) {
1522                 // A chunk snuck in through a race. Release it after restoring MAGAZINE_FREED state.
1523                 next.releaseFromMagazine();
1524             }
1525         }
1526 
1527         private void transferToNextInLineOrRelease(Chunk chunk) {
1528             if (NEXT_IN_LINE.compareAndSet(this, null, chunk)) {
1529                 return;
1530             }
1531 
1532             Chunk nextChunk = NEXT_IN_LINE.get(this);
1533             if (nextChunk != null && nextChunk != MAGAZINE_FREED
1534                     && chunk.remainingCapacity() > nextChunk.remainingCapacity()) {
1535                 if (NEXT_IN_LINE.compareAndSet(this, nextChunk, chunk)) {
1536                     nextChunk.releaseFromMagazine();
1537                     return;
1538                 }
1539             }
1540             // Next-in-line is occupied. We don't try to add it to the central queue yet as it might still be used
1541             // by some buffers and so is attached to a Magazine.
1542             // Once a Chunk is completely released by Chunk.release() it will try to move itself to the queue
1543             // as last resort.
1544             chunk.releaseFromMagazine();
1545         }
1546 
1547         void free() {
1548             // Release the current Chunk and the next that was stored for later usage.
1549             restoreMagazineFreed();
1550             long stamp = allocationLock != null ? allocationLock.writeLock() : 0;
1551             try {
1552                 if (current != null) {
1553                     current.releaseFromMagazine();
1554                     current = null;
1555                 }
1556             } finally {
1557                 if (allocationLock != null) {
1558                     allocationLock.unlockWrite(stamp);
1559                 }
1560             }
1561         }
1562 
1563         public AdaptiveByteBuf newBuffer() {
1564             AdaptiveRecycler recycler = this.recycler;
1565             AdaptiveByteBuf buf = recycler == null ? EVENT_LOOP_LOCAL_BUFFER_POOL.get() : recycler.get();
1566             buf.resetRefCnt();
1567             buf.discardMarks();
1568             return buf;
1569         }
1570 
1571         boolean offerToQueue(Chunk chunk) {
1572             return group.offerChunk(chunk);
1573         }
1574     }
1575 
1576     private static final class ChunkRegistry {
1577         private final LongAdder totalCapacity = new LongAdder();
1578 
1579         public long totalCapacity() {
1580             return totalCapacity.sum();
1581         }
1582 
1583         public void add(Chunk chunk) {
1584             totalCapacity.add(chunk.capacity());
1585         }
1586 
1587         public void remove(Chunk chunk) {
1588             totalCapacity.add(-chunk.capacity());
1589         }
1590     }
1591 
1592     static class Chunk implements ChunkInfo {
1593         protected final AbstractByteBuf delegate;
1594         // the delegate's memory address, or 0 when it has none: it doesn't change while the chunk is in use
1595         final long delegateMemoryAddress;
1596         protected Magazine magazine;
1597         private final AdaptivePoolingAllocator allocator;
1598         // Always populate the refCnt field, so HotSpot doesn't emit `null` checks.
1599         // This is safe to do even on native-image.
1600         private final RefCnt refCnt = new RefCnt();
1601         private final int capacity;
1602         private final boolean pooled;
1603         protected int allocatedBytes;
1604 
1605         Chunk() {
1606             // Constructor only used by the MAGAZINE_FREED sentinel.
1607             delegate = null;
1608             delegateMemoryAddress = 0L;
1609             magazine = null;
1610             allocator = null;
1611             capacity = 0;
1612             pooled = false;
1613         }
1614 
1615         Chunk(AbstractByteBuf delegate, Magazine magazine, boolean pooled) {
1616             this.delegate = delegate;
1617             delegateMemoryAddress = delegate.hasMemoryAddress() ? delegate.memoryAddress() : 0L;
1618             this.pooled = pooled;
1619             capacity = delegate.capacity();
1620             attachToMagazine(magazine);
1621 
1622             // We need the top-level allocator so ByteBuf.capacity(int) can call reallocate()
1623             allocator = magazine.group.allocator;
1624 
1625             if (PlatformDependent.isJfrEnabled() && AllocateChunkEvent.isEventEnabled()) {
1626                 AllocateChunkEvent event = new AllocateChunkEvent();
1627                 if (event.shouldCommit()) {
1628                     event.fill(this, AdaptiveByteBufAllocator.class);
1629                     event.pooled = pooled;
1630                     event.threadLocal = magazine.allocationLock == null;
1631                     event.commit();
1632                 }
1633             }
1634         }
1635 
1636         Magazine currentMagazine() {
1637             return magazine;
1638         }
1639 
1640         void detachFromMagazine() {
1641             if (magazine != null) {
1642                 magazine = null;
1643             }
1644         }
1645 
1646         void attachToMagazine(Magazine magazine) {
1647             assert this.magazine == null;
1648             this.magazine = magazine;
1649         }
1650 
1651         /**
1652          * Called when a magazine is done using this chunk, probably because it was emptied.
1653          */
1654         void releaseFromMagazine() {
1655             // Chunks can be reused before they become empty.
1656             // We can therefor put them in the shared queue as soon as the magazine is done with this chunk.
1657             Magazine mag = magazine;
1658             detachFromMagazine();
1659             if (!mag.offerToQueue(this)) {
1660                 markToDeallocate();
1661             }
1662         }
1663 
1664         /**
1665          * Called when a ByteBuf is done using its allocation in this chunk.
1666          */
1667         void releaseSegment(int ignoredSegmentId, int size) {
1668             release();
1669         }
1670 
1671         void markToDeallocate() {
1672             release();
1673         }
1674 
1675         private void retain() {
1676             RefCnt.retain(refCnt);
1677         }
1678 
1679         protected boolean release() {
1680             boolean deallocate = RefCnt.release(refCnt);
1681             if (deallocate) {
1682                 deallocate();
1683             }
1684             return deallocate;
1685         }
1686 
1687         protected void deallocate() {
1688             onRelease();
1689             allocator.chunkRegistry.remove(this);
1690             delegate.release();
1691         }
1692 
1693         private void onRelease() {
1694             if (PlatformDependent.isJfrEnabled() && FreeChunkEvent.isEventEnabled()) {
1695                 FreeChunkEvent event = new FreeChunkEvent();
1696                 if (event.shouldCommit()) {
1697                     event.fill(this, AdaptiveByteBufAllocator.class);
1698                     event.pooled = pooled;
1699                     event.commit();
1700                 }
1701             }
1702         }
1703 
1704         public boolean readInitInto(AdaptiveByteBuf buf, int size, int startingCapacity, int maxCapacity) {
1705             int startIndex = allocatedBytes;
1706             allocatedBytes = startIndex + startingCapacity;
1707             Chunk chunk = this;
1708             chunk.retain();
1709             try {
1710                 buf.init(delegate, chunk, 0, 0, startIndex, size, startingCapacity, maxCapacity);
1711                 chunk = null;
1712             } finally {
1713                 if (chunk != null) {
1714                     // If chunk is not null we know that buf.init(...) failed and so we need to manually release
1715                     // the chunk again as we retained it before calling buf.init(...). Beside this we also need to
1716                     // restore the old allocatedBytes value.
1717                     allocatedBytes = startIndex;
1718                     chunk.release();
1719                 }
1720             }
1721             return true;
1722         }
1723 
1724         public int remainingCapacity() {
1725             return capacity - allocatedBytes;
1726         }
1727 
1728         public boolean hasUnprocessedFreelistEntries() {
1729             return false;
1730         }
1731 
1732         public void processFreelistEntries() {
1733         }
1734 
1735         @Override
1736         public int capacity() {
1737             return capacity;
1738         }
1739 
1740         @Override
1741         public boolean isDirect() {
1742             return delegate.isDirect();
1743         }
1744 
1745         @Override
1746         public long memoryAddress() {
1747             return delegate._memoryAddress();
1748         }
1749     }
1750 
1751     private static final class IntStack {
1752 
1753         private final int[] stack;
1754         private int top;
1755 
1756         IntStack(int[] initialValues) {
1757             stack = initialValues;
1758             top = initialValues.length - 1;
1759         }
1760 
1761         public boolean isEmpty() {
1762             return top == -1;
1763         }
1764 
1765         public int pop() {
1766             final int last = stack[top];
1767             top--;
1768             return last;
1769         }
1770 
1771         public void push(int value) {
1772             stack[top + 1] = value;
1773             top++;
1774         }
1775 
1776         public int size() {
1777             return top + 1;
1778         }
1779     }
1780 
1781     /**
1782      * Removes per-allocation retain()/release() atomic ops from the hot path by replacing ref counting
1783      * with a segment-count state machine. Atomics are only needed on the cold deallocation path
1784      * ({@link #markToDeallocate()}), which is rare for long-lived chunks that cycle segments many times.
1785      * The tradeoff is a {@link MpscIntQueue#size()} call (volatile reads, no RMW) per remaining segment
1786      * return after mark — acceptable since it avoids atomic RMWs entirely.
1787      * <p>
1788      * State transitions:
1789      * <ul>
1790      *   <li>{@link #AVAILABLE} (-1): chunk is in use, no deallocation tracking needed</li>
1791      *   <li>0..N: local free list size at the time {@link #markToDeallocate()} was called;
1792      *       used to track when all segments have been returned</li>
1793      *   <li>{@link #DEALLOCATED} (Integer.MIN_VALUE): all segments returned, chunk deallocated</li>
1794      * </ul>
1795      * <p>
1796      * Ordering: external {@link #releaseSegment} pushes to the MPSC queue (which has an implicit
1797      * StoreLoad barrier via its {@code offer()}), then reads {@code state} — this guarantees
1798      * visibility of any preceding {@link #markToDeallocate()} write.
1799      */
1800     static class SizeClassedChunk extends Chunk {
1801         private static final int FREE_LIST_EMPTY = -1;
1802         private static final int AVAILABLE = -1;
1803         // Integer.MIN_VALUE so that `DEALLOCATED + externalFreeList.size()` can never equal `segments`,
1804         // making late-arriving releaseSegment calls on external threads arithmetically harmless.
1805         private static final int DEALLOCATED = Integer.MIN_VALUE;
1806         private static final AtomicIntegerFieldUpdater<SizeClassedChunk> STATE =
1807                 AtomicIntegerFieldUpdater.newUpdater(SizeClassedChunk.class, "state");
1808         private volatile int state;
1809         private final int segments;
1810         private final int segmentSize;
1811         private final MpscIntQueue externalFreeList;
1812         private final IntStack localFreeList;
1813         private Thread ownerThread;
1814         int purgeEpoch;
1815         long lastPurgeGeneration;
1816         long lastScanGeneration;
1817 
1818         SizeClassedChunk(AbstractByteBuf delegate, Magazine magazine,
1819                          SizeClassChunkController controller) {
1820             super(delegate, magazine, true);
1821             segmentSize = controller.segmentSize;
1822             segments = controller.chunkSize / segmentSize;
1823             STATE.lazySet(this, AVAILABLE);
1824             ownerThread = magazine.group.ownerThread;
1825             if (ownerThread == null) {
1826                 externalFreeList = controller.createFreeList();
1827                 localFreeList = null;
1828             } else {
1829                 externalFreeList = controller.createEmptyFreeList();
1830                 localFreeList = controller.createLocalFreeList();
1831             }
1832         }
1833 
1834         @Override
1835         public boolean readInitInto(AdaptiveByteBuf buf, int size, int startingCapacity, int maxCapacity) {
1836             assert state == AVAILABLE;
1837             final int startIndex = nextAvailableSegmentOffset();
1838             if (startIndex == FREE_LIST_EMPTY) {
1839                 return false;
1840             }
1841             allocatedBytes += segmentSize;
1842             try {
1843                 buf.init(delegate, this, 0, 0, startIndex, size, startingCapacity, maxCapacity);
1844             } catch (Throwable t) {
1845                 allocatedBytes -= segmentSize;
1846                 releaseSegmentOffsetIntoFreeList(startIndex);
1847                 throw t;
1848             }
1849             return true;
1850         }
1851 
1852         private int nextAvailableSegmentOffset() {
1853             final int startIndex;
1854             IntStack localFreeList = this.localFreeList;
1855             if (localFreeList != null) {
1856                 assert Thread.currentThread() == ownerThread;
1857                 if (localFreeList.isEmpty()) {
1858                     startIndex = externalFreeList.poll();
1859                 } else {
1860                     startIndex = localFreeList.pop();
1861                 }
1862             } else {
1863                 startIndex = externalFreeList.poll();
1864             }
1865             return startIndex;
1866         }
1867 
1868         // this can be used by the ConcurrentQueueChunkCache to find the first buffer to use:
1869         // it doesn't update the remaining capacity and it's not consider a single segmentSize
1870         // case as not suitable to be reused
1871         public boolean hasRemainingCapacity() {
1872             int remaining = super.remainingCapacity();
1873             if (remaining > 0) {
1874                 return true;
1875             }
1876             if (localFreeList != null) {
1877                 return !localFreeList.isEmpty();
1878             }
1879             return !externalFreeList.isEmpty();
1880         }
1881 
1882         boolean hasFullCapacity() {
1883             int free = externalFreeList.size();
1884             IntStack local = localFreeList;
1885             if (local != null) {
1886                 free += local.size();
1887             }
1888             return free == segments;
1889         }
1890 
1891         @Override
1892         public int remainingCapacity() {
1893             int remaining = super.remainingCapacity();
1894             return remaining > segmentSize ? remaining : updateRemainingCapacity(remaining);
1895         }
1896 
1897         private int updateRemainingCapacity(int snapshotted) {
1898             int freeSegments = externalFreeList.size();
1899             IntStack localFreeList = this.localFreeList;
1900             if (localFreeList != null) {
1901                 freeSegments += localFreeList.size();
1902             }
1903             int updated = freeSegments * segmentSize;
1904             if (updated != snapshotted) {
1905                 allocatedBytes = capacity() - updated;
1906             }
1907             return updated;
1908         }
1909 
1910         private void releaseSegmentOffsetIntoFreeList(int startIndex) {
1911             IntStack localFreeList = this.localFreeList;
1912             if (localFreeList != null && Thread.currentThread() == ownerThread) {
1913                 localFreeList.push(startIndex);
1914             } else {
1915                 boolean segmentReturned = externalFreeList.offer(startIndex);
1916                 assert segmentReturned : "Unable to return segment " + startIndex + " to free list";
1917             }
1918         }
1919 
1920         @Override
1921         void releaseSegment(int startIndex, int size) {
1922             IntStack localFreeList = this.localFreeList;
1923             if (localFreeList != null && Thread.currentThread() == ownerThread) {
1924                 localFreeList.push(startIndex);
1925                 int state = this.state;
1926                 if (state != AVAILABLE) {
1927                     updateStateOnLocalReleaseSegment(state, localFreeList);
1928                 }
1929             } else {
1930                 boolean segmentReturned = externalFreeList.offer(startIndex);
1931                 assert segmentReturned;
1932                 // implicit StoreLoad barrier from MPSC offer()
1933                 int state = this.state;
1934                 if (state != AVAILABLE) {
1935                     deallocateIfNeeded(state);
1936                 }
1937             }
1938         }
1939 
1940         private void updateStateOnLocalReleaseSegment(int previousLocalSize, IntStack localFreeList) {
1941             int newLocalSize = localFreeList.size();
1942             boolean alwaysTrue = STATE.compareAndSet(this, previousLocalSize, newLocalSize);
1943             assert alwaysTrue : "this shouldn't happen unless double release in the local free list";
1944             deallocateIfNeeded(newLocalSize);
1945         }
1946 
1947         private void deallocateIfNeeded(int localSize) {
1948             // Check if all segments have been returned.
1949             int totalFreeSegments = localSize + externalFreeList.size();
1950             if (totalFreeSegments == segments && STATE.compareAndSet(this, localSize, DEALLOCATED)) {
1951                 deallocate();
1952             }
1953         }
1954 
1955         @Override
1956         void markToDeallocate() {
1957             IntStack localFreeList = this.localFreeList;
1958             int localSize = localFreeList != null ? localFreeList.size() : 0;
1959             STATE.set(this, localSize);
1960             deallocateIfNeeded(localSize);
1961         }
1962     }
1963 
1964     private static final class BuddyChunk extends Chunk implements IntConsumer {
1965         private static final int MIN_BUDDY_SIZE = 32768;
1966         private static final byte IS_CLAIMED = (byte) (1 << 7);
1967         private static final byte HAS_CLAIMED_CHILDREN = 1 << 6;
1968         private static final byte SHIFT_MASK = ~(IS_CLAIMED | HAS_CLAIMED_CHILDREN);
1969         private static final int PACK_OFFSET_MASK = 0xFFFF;
1970         private static final int PACK_SIZE_SHIFT = Integer.SIZE - Integer.numberOfLeadingZeros(PACK_OFFSET_MASK);
1971 
1972         private final MpscIntQueue freeList;
1973         // The bits of each buddy: [1: is claimed][1: has claimed children][30: MIN_BUDDY_SIZE shift to get size]
1974         private final byte[] buddies;
1975         private final int freeListCapacity;
1976 
1977         BuddyChunk(AbstractByteBuf delegate, Magazine magazine) {
1978             super(delegate, magazine, true);
1979             freeListCapacity = delegate.capacity() / MIN_BUDDY_SIZE;
1980             int maxShift = Integer.numberOfTrailingZeros(freeListCapacity);
1981             assert maxShift <= 30; // The top 2 bits are used for marking.
1982             freeList = MpscIntQueue.create(freeListCapacity, -1); // At most half of tree (all leaf nodes) can be freed.
1983             buddies = new byte[freeListCapacity << 1];
1984 
1985             // Generate the buddies entries.
1986             int index = 1;
1987             int runLength = 1;
1988             int currentRun = 0;
1989             while (maxShift > 0) {
1990                 buddies[index++] = (byte) maxShift;
1991                 if (++currentRun == runLength) {
1992                     currentRun = 0;
1993                     runLength <<= 1;
1994                     maxShift--;
1995                 }
1996             }
1997         }
1998 
1999         @Override
2000         public boolean readInitInto(AdaptiveByteBuf buf, int size, int startingCapacity, int maxCapacity) {
2001             if (!freeList.isEmpty()) {
2002                 freeList.drain(freeListCapacity, this);
2003             }
2004             int startIndex = chooseFirstFreeBuddy(1, startingCapacity, 0);
2005             if (startIndex == -1) {
2006                 return false;
2007             }
2008             Chunk chunk = this;
2009             chunk.retain();
2010             try {
2011                 buf.init(delegate, this, 0, 0, startIndex, size, startingCapacity, maxCapacity);
2012                 allocatedBytes += startingCapacity;
2013                 chunk = null;
2014             } finally {
2015                 if (chunk != null) {
2016                     unreserveMatchingBuddy(1, startingCapacity, startIndex, 0);
2017                     // If chunk is not null we know that buf.init(...) failed and so we need to manually release
2018                     // the chunk again as we retained it before calling buf.init(...).
2019                     chunk.release();
2020                 }
2021             }
2022             return true;
2023         }
2024 
2025         @Override
2026         public void accept(int packed) {
2027             // Called by allocating thread when draining freeList.
2028             int size = unpackSize(packed);
2029             int offset = unpackOffset(packed);
2030             unreserveMatchingBuddy(1, size, offset, 0);
2031             allocatedBytes -= size;
2032         }
2033 
2034         private static int unpackSize(int packed) {
2035             return MIN_BUDDY_SIZE << (packed >> PACK_SIZE_SHIFT);
2036         }
2037 
2038         private static int unpackOffset(int packed) {
2039             return (packed & PACK_OFFSET_MASK) * MIN_BUDDY_SIZE;
2040         }
2041 
2042         @Override
2043         void releaseSegment(int startingIndex, int size) {
2044             int packedOffset = startingIndex / MIN_BUDDY_SIZE;
2045             int packedSize = Integer.numberOfTrailingZeros(size / MIN_BUDDY_SIZE) << PACK_SIZE_SHIFT;
2046             int packed = packedOffset | packedSize;
2047             freeList.offer(packed);
2048             release();
2049         }
2050 
2051         @Override
2052         public int remainingCapacity() {
2053             int capacityInFreeList = 0;
2054             if (!freeList.isEmpty()) {
2055                 capacityInFreeList = freeList.weakPeekReduce(freeListCapacity, 0,
2056                         (sum, entry) -> sum + unpackSize(entry));
2057             }
2058             return super.remainingCapacity() + capacityInFreeList;
2059         }
2060 
2061         @Override
2062         public boolean hasUnprocessedFreelistEntries() {
2063             return !freeList.isEmpty();
2064         }
2065 
2066         @Override
2067         public void processFreelistEntries() {
2068             freeList.drain(freeListCapacity, this);
2069         }
2070 
2071         /**
2072          * Claim a suitable buddy and return its start offset into the delegate chunk, or return -1 if nothing claimed.
2073          */
2074         private int chooseFirstFreeBuddy(int index, int size, int currOffset) {
2075             byte[] buddies = this.buddies;
2076             while (index < buddies.length) {
2077                 byte buddy = buddies[index];
2078                 int currValue = MIN_BUDDY_SIZE << (buddy & SHIFT_MASK);
2079                 if (currValue < size || (buddy & IS_CLAIMED) == IS_CLAIMED) {
2080                     return -1;
2081                 }
2082                 if (currValue == size && (buddy & HAS_CLAIMED_CHILDREN) == 0) {
2083                     buddies[index] |= IS_CLAIMED;
2084                     return currOffset;
2085                 }
2086                 int found = chooseFirstFreeBuddy(index << 1, size, currOffset);
2087                 if (found != -1) {
2088                     buddies[index] |= HAS_CLAIMED_CHILDREN;
2089                     return found;
2090                 }
2091                 index = (index << 1) + 1;
2092                 currOffset += currValue >> 1; // Bump offset to skip first half of this layer.
2093             }
2094             return -1;
2095         }
2096 
2097         /**
2098          * Un-reserve the matching buddy and return whether there are any other child or sibling reservations.
2099          */
2100         private boolean unreserveMatchingBuddy(int index, int size, int offset, int currOffset) {
2101             byte[] buddies = this.buddies;
2102             if (buddies.length <= index) {
2103                 return false;
2104             }
2105             byte buddy = buddies[index];
2106             int currSize = MIN_BUDDY_SIZE << (buddy & SHIFT_MASK);
2107 
2108             if (currSize == size) {
2109                 // We're at the right size level.
2110                 if (currOffset == offset) {
2111                     buddies[index] &= SHIFT_MASK;
2112                     return false;
2113                 }
2114                 throw new IllegalStateException("The intended segment was not found at index " +
2115                         index + ", for size " + size + " and offset " + offset);
2116             }
2117 
2118             // We're at a parent size level. Use the target offset to guide our drill-down path.
2119             boolean claims;
2120             int siblingIndex;
2121             if (offset < currOffset + (currSize >> 1)) {
2122                 // Must be down the left path.
2123                 claims = unreserveMatchingBuddy(index << 1, size, offset, currOffset);
2124                 siblingIndex = (index << 1) + 1;
2125             } else {
2126                 // Must be down the rigth path.
2127                 claims = unreserveMatchingBuddy((index << 1) + 1, size, offset, currOffset + (currSize >> 1));
2128                 siblingIndex = index << 1;
2129             }
2130             if (!claims) {
2131                 // No other claims down the path we took. Check if the sibling has claims.
2132                 byte sibling = buddies[siblingIndex];
2133                 if ((sibling & SHIFT_MASK) == sibling) {
2134                     // No claims in the sibling. We can clear this level as well.
2135                     buddies[index] &= SHIFT_MASK;
2136                     return false;
2137                 }
2138             }
2139             return true;
2140         }
2141 
2142         @Override
2143         public String toString() {
2144             int capacity = delegate.capacity();
2145             int remaining = capacity - allocatedBytes;
2146             return "BuddyChunk[capacity: " + capacity +
2147                     ", remaining: " + remaining +
2148                     ", free list: " + freeList.size() + ']';
2149         }
2150     }
2151 
2152     static final class AdaptiveByteBuf extends AbstractReferenceCountedByteBuf {
2153 
2154         private final EnhancedHandle<AdaptiveByteBuf> handle;
2155 
2156         // this both act as adjustment and the start index for a free list segment allocation
2157         private int startIndex;
2158         private AbstractByteBuf rootParent;
2159         // this buffer's memory address, from the root buffer's address that its chunk computed, or 0 when the root
2160         // buffer has none, see _memoryAddress()
2161         private long memoryAddress;
2162         Chunk chunk;
2163         private int length;
2164         private int maxFastCapacity;
2165         private ByteBuffer tmpNioBuf;
2166         private boolean hasArray;
2167         private boolean hasMemoryAddress;
2168 
2169         AdaptiveByteBuf(EnhancedHandle<AdaptiveByteBuf> recyclerHandle) {
2170             super(0);
2171             handle = ObjectUtil.checkNotNull(recyclerHandle, "recyclerHandle");
2172         }
2173 
2174         void init(AbstractByteBuf unwrapped, Chunk wrapped, int readerIndex, int writerIndex,
2175                   int startIndex, int size, int capacity, int maxCapacity) {
2176             this.startIndex = startIndex;
2177             chunk = wrapped;
2178             length = size;
2179             maxFastCapacity = capacity;
2180             maxCapacity(maxCapacity);
2181             setIndex0(readerIndex, writerIndex);
2182             hasArray = unwrapped.hasArray();
2183             hasMemoryAddress = unwrapped.hasMemoryAddress();
2184             // the chunk's delegate is the root buffer
2185             memoryAddress = hasMemoryAddress ? wrapped.delegateMemoryAddress + startIndex : 0L;
2186             rootParent = unwrapped;
2187             tmpNioBuf = null;
2188 
2189             if (PlatformDependent.isJfrEnabled() && AllocateBufferEvent.isEventEnabled()) {
2190                 AllocateBufferEvent event = new AllocateBufferEvent();
2191                 if (event.shouldCommit()) {
2192                     event.fill(this, AdaptiveByteBufAllocator.class);
2193                     event.chunkPooled = wrapped.pooled;
2194                     Magazine m = wrapped.magazine;
2195                     event.chunkThreadLocal = m != null && m.allocationLock == null;
2196                     event.commit();
2197                 }
2198             }
2199         }
2200 
2201         private AbstractByteBuf rootParent() {
2202             final AbstractByteBuf rootParent = this.rootParent;
2203             if (rootParent != null) {
2204                 return rootParent;
2205             }
2206             throw new IllegalReferenceCountException();
2207         }
2208 
2209         @Override
2210         public int capacity() {
2211             return length;
2212         }
2213 
2214         @Override
2215         public int maxFastWritableBytes() {
2216             return Math.min(maxFastCapacity, maxCapacity()) - writerIndex;
2217         }
2218 
2219         @Override
2220         public ByteBuf capacity(int newCapacity) {
2221             checkNewCapacity(newCapacity);
2222             if (length <= newCapacity && newCapacity <= maxFastCapacity) {
2223                 length = newCapacity;
2224                 return this;
2225             }
2226             if (newCapacity < capacity()) {
2227                 length = newCapacity;
2228                 trimIndicesToCapacity(newCapacity);
2229                 return this;
2230             }
2231 
2232             if (PlatformDependent.isJfrEnabled() && ReallocateBufferEvent.isEventEnabled()) {
2233                 ReallocateBufferEvent event = new ReallocateBufferEvent();
2234                 if (event.shouldCommit()) {
2235                     event.fill(this, AdaptiveByteBufAllocator.class);
2236                     event.newCapacity = newCapacity;
2237                     event.commit();
2238                 }
2239             }
2240 
2241             // Reallocation required.
2242             Chunk chunk = this.chunk;
2243             AdaptivePoolingAllocator allocator = chunk.allocator;
2244             int readerIndex = this.readerIndex;
2245             int writerIndex = this.writerIndex;
2246             int baseOldRootIndex = startIndex;
2247             int oldLength = length;
2248             int oldCapacity = maxFastCapacity;
2249             AbstractByteBuf oldRoot = rootParent();
2250             allocator.reallocate(newCapacity, maxCapacity(), this);
2251             oldRoot.getBytes(baseOldRootIndex, this, 0, oldLength);
2252             chunk.releaseSegment(baseOldRootIndex, oldCapacity);
2253             assert oldCapacity < maxFastCapacity && newCapacity <= maxFastCapacity :
2254                     "Capacity increase failed";
2255             this.readerIndex = readerIndex;
2256             this.writerIndex = writerIndex;
2257             return this;
2258         }
2259 
2260         @Override
2261         public ByteBufAllocator alloc() {
2262             return rootParent().alloc();
2263         }
2264 
2265         @SuppressWarnings("deprecation")
2266         @Override
2267         public ByteOrder order() {
2268             return rootParent().order();
2269         }
2270 
2271         @Override
2272         public ByteBuf unwrap() {
2273             return null;
2274         }
2275 
2276         @Override
2277         public boolean isDirect() {
2278             return rootParent().isDirect();
2279         }
2280 
2281         @Override
2282         public int arrayOffset() {
2283             return idx(rootParent().arrayOffset());
2284         }
2285 
2286         @Override
2287         public boolean hasMemoryAddress() {
2288             return hasMemoryAddress;
2289         }
2290 
2291         @Override
2292         public long memoryAddress() {
2293             ensureAccessible();
2294             if (!hasMemoryAddress) {
2295                 throw new UnsupportedOperationException();
2296             }
2297             return _memoryAddress();
2298         }
2299 
2300         @Override
2301         long _memoryAddress() {
2302             // Use the address kept at init() instead of asking the root buffer, which would check its accessibility
2303             // on every call. The root stays accessible while this buffer holds a segment of its chunk. The address is
2304             // 0 when the root buffer has none, and after deallocation.
2305             return memoryAddress;
2306         }
2307 
2308         @Override
2309         boolean _isDirect() {
2310             AbstractByteBuf root = rootParent;
2311             return root != null && root.isDirect();
2312         }
2313 
2314         @Override
2315         public ByteBuffer nioBuffer(int index, int length) {
2316             checkIndex(index, length);
2317             return rootParent().nioBuffer(idx(index), length);
2318         }
2319 
2320         @Override
2321         public ByteBuffer internalNioBuffer(int index, int length) {
2322             checkIndex(index, length);
2323             return (ByteBuffer) internalNioBuffer().position(index).limit(index + length);
2324         }
2325 
2326         private ByteBuffer internalNioBuffer() {
2327             if (tmpNioBuf == null) {
2328                 tmpNioBuf = rootParent().nioBuffer(startIndex, maxFastCapacity);
2329             }
2330             return (ByteBuffer) tmpNioBuf.clear();
2331         }
2332 
2333         @Override
2334         public ByteBuffer[] nioBuffers(int index, int length) {
2335             checkIndex(index, length);
2336             return rootParent().nioBuffers(idx(index), length);
2337         }
2338 
2339         @Override
2340         public boolean hasArray() {
2341             return hasArray;
2342         }
2343 
2344         @Override
2345         public byte[] array() {
2346             ensureAccessible();
2347             return rootParent().array();
2348         }
2349 
2350         @Override
2351         public ByteBuf copy(int index, int length) {
2352             checkIndex(index, length);
2353             return rootParent().copy(idx(index), length);
2354         }
2355 
2356         @Override
2357         public int nioBufferCount() {
2358             return rootParent().nioBufferCount();
2359         }
2360 
2361         @Override
2362         protected byte _getByte(int index) {
2363             return rootParent()._getByte(idx(index));
2364         }
2365 
2366         @Override
2367         protected short _getShort(int index) {
2368             return rootParent()._getShort(idx(index));
2369         }
2370 
2371         @Override
2372         protected short _getShortLE(int index) {
2373             return rootParent()._getShortLE(idx(index));
2374         }
2375 
2376         @Override
2377         protected int _getUnsignedMedium(int index) {
2378             return rootParent()._getUnsignedMedium(idx(index));
2379         }
2380 
2381         @Override
2382         protected int _getUnsignedMediumLE(int index) {
2383             return rootParent()._getUnsignedMediumLE(idx(index));
2384         }
2385 
2386         @Override
2387         protected int _getInt(int index) {
2388             return rootParent()._getInt(idx(index));
2389         }
2390 
2391         @Override
2392         protected int _getIntLE(int index) {
2393             return rootParent()._getIntLE(idx(index));
2394         }
2395 
2396         @Override
2397         protected long _getLong(int index) {
2398             return rootParent()._getLong(idx(index));
2399         }
2400 
2401         @Override
2402         protected long _getLongLE(int index) {
2403             return rootParent()._getLongLE(idx(index));
2404         }
2405 
2406         @Override
2407         public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) {
2408             checkIndex(index, length);
2409             rootParent().getBytes(idx(index), dst, dstIndex, length);
2410             return this;
2411         }
2412 
2413         @Override
2414         public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
2415             checkIndex(index, length);
2416             rootParent().getBytes(idx(index), dst, dstIndex, length);
2417             return this;
2418         }
2419 
2420         @Override
2421         public ByteBuf getBytes(int index, ByteBuffer dst) {
2422             checkIndex(index, dst.remaining());
2423             rootParent().getBytes(idx(index), dst);
2424             return this;
2425         }
2426 
2427         @Override
2428         protected void _setByte(int index, int value) {
2429             rootParent()._setByte(idx(index), value);
2430         }
2431 
2432         @Override
2433         protected void _setShort(int index, int value) {
2434             rootParent()._setShort(idx(index), value);
2435         }
2436 
2437         @Override
2438         protected void _setShortLE(int index, int value) {
2439             rootParent()._setShortLE(idx(index), value);
2440         }
2441 
2442         @Override
2443         protected void _setMedium(int index, int value) {
2444             rootParent()._setMedium(idx(index), value);
2445         }
2446 
2447         @Override
2448         protected void _setMediumLE(int index, int value) {
2449             rootParent()._setMediumLE(idx(index), value);
2450         }
2451 
2452         @Override
2453         protected void _setInt(int index, int value) {
2454             rootParent()._setInt(idx(index), value);
2455         }
2456 
2457         @Override
2458         protected void _setIntLE(int index, int value) {
2459             rootParent()._setIntLE(idx(index), value);
2460         }
2461 
2462         @Override
2463         protected void _setLong(int index, long value) {
2464             rootParent()._setLong(idx(index), value);
2465         }
2466 
2467         @Override
2468         protected void _setLongLE(int index, long value) {
2469             rootParent()._setLongLE(idx(index), value);
2470         }
2471 
2472         @Override
2473         public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
2474             checkIndex(index, length);
2475             if (tmpNioBuf == null && PlatformDependent.javaVersion() >= 13) {
2476                 ByteBuffer dstBuffer = rootParent()._internalNioBuffer();
2477                 PlatformDependent.absolutePut(dstBuffer, idx(index), src, srcIndex, length);
2478             } else {
2479                 ByteBuffer tmp = (ByteBuffer) internalNioBuffer().position(index);
2480                 tmp.put(src, srcIndex, length);
2481             }
2482             return this;
2483         }
2484 
2485         @Override
2486         public ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) {
2487             if (PlatformDependent.hasUnsafe() && hasMemoryAddress) {
2488                 // Copies from a source with a memory address or an array without an NIO buffer view of the source,
2489                 // which src.nioBuffer(...) below would allocate for each copy from a buffer of another kind, such
2490                 // as a slice or a buffer of another allocator. Without Unsafe, a direct buffer can still have a
2491                 // memory address (Java 25+), but the copy needs Unsafe; checking hasUnsafe() first lets the JIT
2492                 // drop this branch then.
2493                 UnsafeByteBufUtil.setBytes(this, _memoryAddress() + index, index, src, srcIndex, length);
2494                 return this;
2495             }
2496             checkIndex(index, length);
2497             if (src instanceof AdaptiveByteBuf && PlatformDependent.javaVersion() >= 16) {
2498                 AdaptiveByteBuf srcBuf = (AdaptiveByteBuf) src;
2499                 srcBuf.checkIndex(srcIndex, length);
2500                 ByteBuffer dstBuffer = rootParent()._internalNioBuffer();
2501                 ByteBuffer srcBuffer = srcBuf.rootParent()._internalNioBuffer();
2502                 PlatformDependent.absolutePut(dstBuffer, idx(index), srcBuffer, srcBuf.idx(srcIndex), length);
2503             } else {
2504                 ByteBuffer tmp = internalNioBuffer();
2505                 tmp.position(index);
2506                 tmp.put(src.nioBuffer(srcIndex, length));
2507             }
2508             return this;
2509         }
2510 
2511         @Override
2512         public ByteBuf setBytes(int index, ByteBuffer src) {
2513             int length = src.remaining();
2514             checkIndex(index, length);
2515             if (src == tmpNioBuf) {
2516                 src = src.duplicate();
2517             }
2518             ByteBuffer tmp = internalNioBuffer();
2519             if (PlatformDependent.javaVersion() >= 16) {
2520                 int offset = src.position();
2521                 PlatformDependent.absolutePut(tmp, index, src, offset, length);
2522                 src.position(offset + length);
2523             } else {
2524                 tmp.position(index);
2525                 tmp.put(src);
2526             }
2527             return this;
2528         }
2529 
2530         @Override
2531         public ByteBuf getBytes(int index, OutputStream out, int length)
2532                 throws IOException {
2533             checkIndex(index, length);
2534             if (length != 0) {
2535                 ByteBuffer tmp = internalNioBuffer();
2536                 ByteBufUtil.readBytes(alloc(), tmp.hasArray() ? tmp : tmp.duplicate(), index, length, out);
2537             }
2538             return this;
2539         }
2540 
2541         @Override
2542         public int getBytes(int index, GatheringByteChannel out, int length)
2543                 throws IOException {
2544             checkIndex(index, length);
2545             ByteBuffer buf = internalNioBuffer().duplicate();
2546             buf.position(index).limit(index + length);
2547             return out.write(buf);
2548         }
2549 
2550         @Override
2551         public int getBytes(int index, FileChannel out, long position, int length)
2552                 throws IOException {
2553             checkIndex(index, length);
2554             ByteBuffer buf = internalNioBuffer().duplicate();
2555             buf.position(index).limit(index + length);
2556             return out.write(buf, position);
2557         }
2558 
2559         @Override
2560         public int setBytes(int index, InputStream in, int length)
2561                 throws IOException {
2562             checkIndex(index, length);
2563             final AbstractByteBuf rootParent = rootParent();
2564             if (rootParent.hasArray()) {
2565                 return rootParent.setBytes(idx(index), in, length);
2566             }
2567             byte[] tmp = ByteBufUtil.threadLocalTempArray(length);
2568             int readBytes = in.read(tmp, 0, length);
2569             if (readBytes <= 0) {
2570                 return readBytes;
2571             }
2572             setBytes(index, tmp, 0, readBytes);
2573             return readBytes;
2574         }
2575 
2576         @Override
2577         public int setBytes(int index, ScatteringByteChannel in, int length)
2578                 throws IOException {
2579             try {
2580                 return in.read(internalNioBuffer(index, length));
2581             } catch (ClosedChannelException ignored) {
2582                 return -1;
2583             }
2584         }
2585 
2586         @Override
2587         public int setBytes(int index, FileChannel in, long position, int length)
2588                 throws IOException {
2589             try {
2590                 return in.read(internalNioBuffer(index, length), position);
2591             } catch (ClosedChannelException ignored) {
2592                 return -1;
2593             }
2594         }
2595 
2596         @Override
2597         public int setCharSequence(int index, CharSequence sequence, Charset charset) {
2598             return setCharSequence0(index, sequence, charset, false);
2599         }
2600 
2601         private int setCharSequence0(int index, CharSequence sequence, Charset charset, boolean expand) {
2602             if (charset.equals(CharsetUtil.UTF_8)) {
2603                 int length = ByteBufUtil.utf8MaxBytes(sequence);
2604                 if (expand) {
2605                     ensureWritable0(length);
2606                     checkIndex0(index, length);
2607                 } else {
2608                     checkIndex(index, length);
2609                 }
2610                 return ByteBufUtil.writeUtf8(this, index, length, sequence, sequence.length());
2611             }
2612             if (charset.equals(CharsetUtil.US_ASCII) || charset.equals(CharsetUtil.ISO_8859_1)) {
2613                 int length = sequence.length();
2614                 if (expand) {
2615                     ensureWritable0(length);
2616                     checkIndex0(index, length);
2617                 } else {
2618                     checkIndex(index, length);
2619                 }
2620                 return ByteBufUtil.writeAscii(this, index, sequence, length);
2621             }
2622             byte[] bytes = sequence.toString().getBytes(charset);
2623             if (expand) {
2624                 ensureWritable0(bytes.length);
2625                 // setBytes(...) will take care of checking the indices.
2626             }
2627             setBytes(index, bytes);
2628             return bytes.length;
2629         }
2630 
2631         @Override
2632         public int writeCharSequence(CharSequence sequence, Charset charset) {
2633             int written = setCharSequence0(writerIndex, sequence, charset, true);
2634             writerIndex += written;
2635             return written;
2636         }
2637 
2638         @Override
2639         public int forEachByte(int index, int length, ByteProcessor processor) {
2640             checkIndex(index, length);
2641             int ret = rootParent().forEachByte(idx(index), length, processor);
2642             return forEachResult(ret);
2643         }
2644 
2645         @Override
2646         public int forEachByteDesc(int index, int length, ByteProcessor processor) {
2647             checkIndex(index, length);
2648             int ret = rootParent().forEachByteDesc(idx(index), length, processor);
2649             return forEachResult(ret);
2650         }
2651 
2652         @Override
2653         public ByteBuf setZero(int index, int length) {
2654             checkIndex(index, length);
2655             rootParent().setZero(idx(index), length);
2656             return this;
2657         }
2658 
2659         @Override
2660         public ByteBuf writeZero(int length) {
2661             ensureWritable(length);
2662             rootParent().setZero(idx(writerIndex), length);
2663             writerIndex += length;
2664             return this;
2665         }
2666 
2667         private int forEachResult(int ret) {
2668             if (ret < startIndex) {
2669                 return -1;
2670             }
2671             return ret - startIndex;
2672         }
2673 
2674         @Override
2675         public boolean isContiguous() {
2676             return rootParent().isContiguous();
2677         }
2678 
2679         private int idx(int index) {
2680             return index + startIndex;
2681         }
2682 
2683         @Override
2684         protected void deallocate() {
2685             if (PlatformDependent.isJfrEnabled() && FreeBufferEvent.isEventEnabled()) {
2686                 FreeBufferEvent event = new FreeBufferEvent();
2687                 if (event.shouldCommit()) {
2688                     event.fill(this, AdaptiveByteBufAllocator.class);
2689                     event.commit();
2690                 }
2691             }
2692 
2693             if (chunk != null) {
2694                 chunk.releaseSegment(startIndex, maxFastCapacity);
2695             }
2696             tmpNioBuf = null;
2697             chunk = null;
2698             rootParent = null;
2699             memoryAddress = 0L;
2700             hasMemoryAddress = false;
2701             handle.unguardedRecycle(this);
2702         }
2703     }
2704 
2705     /**
2706      * The strategy for how {@link AdaptivePoolingAllocator} should allocate chunk buffers.
2707      */
2708     interface ChunkAllocator {
2709         /**
2710          * Allocate a buffer for a chunk. This can be any kind of {@link AbstractByteBuf} implementation.
2711          *
2712          * @param initialCapacity The initial capacity of the returned {@link AbstractByteBuf}.
2713          * @param maxCapacity     The maximum capacity of the returned {@link AbstractByteBuf}.
2714          * @return The buffer that represents the chunk memory.
2715          */
2716         AbstractByteBuf allocate(int initialCapacity, int maxCapacity);
2717     }
2718 }