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