Inside Netty 4.2's AdaptiveByteBufAllocator: Stripe-Based Allocation & Lock-Free Strategies
This article dissects Netty 4.2's AdaptivePoolingAllocator memory allocation flow, covering request size routing, MagazineGroup's stripe mechanism with dynamic expansion, and Magazine's tryAllocate implementation with locked and lock-free paths using NEXT_IN_LINE for concurrency safety.
1. Request Size Routing
The allocation entry point is AdaptivePoolingAllocator.allocate. For requests ≤ MAX_POOLED_BUF_SIZE (default 1 MB), the allocator computes a size‑class index via sizeClassIndexOf. If the current thread supports thread‑local storage (typically an EventLoop thread), it uses the thread‑local magazineGroups; otherwise it falls back to the shared sizeClassedMagazineGroups. When the index is within bounds, the corresponding MagazineGroup handles allocation; otherwise the largeBufferMagazineGroup is used. Requests larger than MAX_POOLED_BUF_SIZE or failed allocations go to allocateFallback, which creates a one‑off Chunk attached to the large‑buffer group.
ByteBuf allocate(int size, int maxCapacity) {
return allocate(size, maxCapacity, Thread.currentThread(), null);
}
private AdaptiveByteBuf allocate(int size, int maxCapacity, Thread currentThread, AdaptiveByteBuf buf) {
AdaptiveByteBuf allocated = null;
if (size <= MAX_POOLED_BUF_SIZE) {
final int index = sizeClassIndexOf(size);
MagazineGroup[] magazineGroups;
if (!FastThreadLocalThread.currentThreadWillCleanupFastThreadLocals() ||
IS_LOW_MEM ||
(magazineGroups = threadLocalGroup.get()) == null) {
magazineGroups = sizeClassedMagazineGroups;
}
if (index < magazineGroups.length) {
allocated = magazineGroups[index].allocate(size, maxCapacity, currentThread, buf);
} else if (!IS_LOW_MEM) {
allocated = largeBufferMagazineGroup.allocate(size, maxCapacity, currentThread, buf);
}
}
if (allocated == null) {
allocated = allocateFallback(size, maxCapacity, currentThread, buf);
}
return allocated;
} IS_LOW_MEMdefaults to false on servers (threshold 512 MB). The fallback path directly instantiates a Chunk via chunkAllocator.allocate, wraps it, initializes the buffer, and immediately releases the chunk because it is a one‑off allocation.
2. MagazineGroup Management
2.1 Thread‑Local Group
The thread‑local fast path is straightforward: if a thread‑local Magazine exists, it creates a buffer via newBuffer and calls tryAllocate, which must succeed (asserted).
public AdaptiveByteBuf allocate(int size, int maxCapacity, Thread currentThread, AdaptiveByteBuf buf) {
boolean reallocate = buf != null;
Magazine tlMag = threadLocalMagazine;
if (tlMag != null) {
if (buf == null) {
buf = tlMag.newBuffer();
}
boolean allocated = tlMag.tryAllocate(size, maxCapacity, buf, reallocate);
assert allocated : "Allocation of threadLocalMagazine must always succeed";
return buf;
}
// ... shared path omitted
}2.2 Shared & Large Object Groups
Both shared and large‑object groups use the same MagazineGroup logic; they differ only in the ChunkControllerFactory implementation. The group holds an array of Magazine instances (stripes). A thread picks a stripe via threadId & (mags.length - 1) and scans up to mags.length << 1 magazines. If all attempts fail, the group tries to expand the magazine array (doubling its size) up to EXPANSION_ATTEMPTS times. If expansion fails or is exhausted, allocation returns null.
public AdaptiveByteBuf allocate(int size, int maxCapacity, Thread currentThread, AdaptiveByteBuf buf) {
boolean reallocate = buf != null;
long threadId = currentThread.getId();
Magazine[] mags;
int expansions = 0;
do {
mags = magazines;
int mask = mags.length - 1;
int index = (int) (threadId & mask);
for (int i = 0, m = mags.length << 1; i < m; i++) {
Magazine mag = mags[index + i & mask];
if (buf == null) {
buf = mag.newBuffer();
}
if (mag.tryAllocate(size, maxCapacity, buf, reallocate)) {
return buf;
}
}
expansions++;
} while (expansions <= EXPANSION_ATTEMPTS && tryExpandMagazines(mags.length));
if (!reallocate && buf != null) {
buf.release();
}
return null;
}The stripe array reduces contention by partitioning locks. After obtaining a magazine, the allocator first calls mag.newBuffer() to get a wrapper buffer, then mag.tryAllocate() to perform the actual memory assignment. The author notes a potential concurrency concern: during expansion the old magazine array is freed while allocations may still be in progress on the old magazines. This race is resolved at the Magazine layer via NEXT_IN_LINE.
3. Magazine Allocation
3.1 newBuffer
newBufferrecycles AdaptiveByteBuf wrappers. For thread‑local magazines ( handle == null) it uses a Recycler; for shared magazines it uses a FixedMpmcQueue (capacity 1024).
public AdaptiveByteBuf newBuffer() {
AdaptiveByteBuf buf;
if (handle == null) {
buf = EVENT_LOOP_LOCAL_BUFFER_POOL.get();
} else {
buf = bufferQueue.poll();
if (buf == null) {
buf = new AdaptiveByteBuf(handle);
}
}
buf.resetRefCnt();
buf.discardMarks();
return buf;
}3.2 tryAllocate
tryAllocatebranches into three paths:
Lock‑free (thread‑local) : allocationLock == null → call allocate directly.
Locked : attempt tryWriteLock; on success call allocate and release the lock.
Lock‑free fallback : if lock acquisition fails, call allocateWithoutLock.
public boolean tryAllocate(int size, int maxCapacity, AdaptiveByteBuf buf, boolean reallocate) {
if (allocationLock == null) {
return allocate(size, maxCapacity, buf, reallocate);
}
long writeLock = allocationLock.tryWriteLock();
if (writeLock != 0) {
try {
return allocate(size, maxCapacity, buf, reallocate);
} finally {
allocationLock.unlockWrite(writeLock);
}
}
return allocateWithoutLock(size, maxCapacity, buf);
}Locked Allocation (allocate)
The allocate method (117 lines) follows three sub‑paths:
Fast path : allocate from the current Chunk.
Concurrent path : allocate from NEXT_IN_LINE.
Slow path : obtain a Chunk from chunkReuseQueue or create a new one, then allocate.
The author criticizes NEXT_IN_LINE for violating the Single Responsibility Principle, as it serves both performance optimization (reducing chunkReuseQueue access, providing a ready chunk for lock‑free allocation) and concurrency safety (marked MAGAZINE_FREED on magazine release to prevent races and leaks). This dual role resolves the expansion‑race mentioned in section 2.2.
Lock‑Free Allocation (allocateWithoutLock)
Lock‑free allocation operates on NEXT_IN_LINE using an atomic getAndSet:
private boolean allocateWithoutLock(int size, int maxCapacity, AdaptiveByteBuf buf) {
Chunk curr = NEXT_IN_LINE.getAndSet(this, null);
if (curr == MAGAZINE_FREED) {
restoreMagazineFreed();
return false;
}
if (curr == null) {
curr = sharedChunkQueue.poll();
if (curr == null) {
return false;
}
curr.attachToMagazine(this);
}
boolean allocated = false;
int remainingCapacity = curr.remainingCapacity();
int startingCapacity = chunkController.computeBufferCapacity(
size, maxCapacity, true /* never update stats */);
if (remainingCapacity >= size) {
curr.readInitInto(buf, size, Math.min(remainingCapacity, startingCapacity), maxCapacity);
allocated = true;
}
try {
if (remainingCapacity >= RETIRE_CAPACITY) {
transferToNextInLineOrRelease(curr);
curr = null;
}
} finally {
if (curr != null) {
curr.releaseFromMagazine();
}
}
return allocated;
}Without the lock, the acquired Chunk can only be stored in NEXT_IN_LINE. If the magazine was freed concurrently ( MAGAZINE_FREED), the allocation aborts and the magazine is restored. If the chunk has sufficient remaining capacity, the buffer is initialized; otherwise the chunk is returned to the pool or transferred to NEXT_IN_LINE for the next allocation.
4. Summary
The author highlights two key adaptive mechanisms in AdaptivePoolingAllocator: MagazineGroup 's stripe partitioning with dynamic array expansion. Magazine 's NEXT_IN_LINE lock‑free allocation strategy.
The ChunkController (to be covered next) embodies the runtime auto‑tuning behavior. The analysis is based on Netty commit a8e370d (upcoming 4.2.8.Final). Reference: https://github.com/netty/netty/commits/4.2/buffer/src/main/java/io/netty/buffer/AdaptivePoolingAllocator.java
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
