How Jackson Rewrote Buffer Pooling for Java Virtual Threads

Jackson replaced its ThreadLocal-based buffer recycling with a pluggable RecyclerPool abstraction, iterated through multiple default pool implementations including a flawed LockFreePool, removed the leaky LockFreePool in 3.x, and fixed a TOCTOU race in BufferRecycler using atomic updates.

Code Archaeology
Code Archaeology
Code Archaeology
How Jackson Rewrote Buffer Pooling for Java Virtual Threads

1. Initial State: ThreadLocal Buffer Recycling

Jackson's JSON parsing and generation repeatedly allocates byte[] and char[] buffers. To avoid frequent allocation, it caches BufferRecycler instances per thread using a ThreadLocal<SoftReference<BufferRecycler>>. The getBufferRecycler() method retrieves or creates a recycler, storing it back into the ThreadLocal. Under traditional platform thread pools, this works well: zero contention, low overhead, and SoftReference allows GC under memory pressure.

final protected static ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>();

public static BufferRecycler getBufferRecycler() {
    SoftReference<BufferRecycler> ref = _recyclerRef.get();
    BufferRecycler br = (ref == null) ? null : ref.get();
    if (br == null) {
        br = new BufferRecycler();
        if (_bufferRecyclerTracker != null) {
            ref = _bufferRecyclerTracker.wrapAndTrack(br);
        } else {
            ref = new SoftReference<>(br);
        }
        _recyclerRef.set(ref);
    }
    return br;
}

2. Virtual Threads Break ThreadLocal Assumptions

Virtual threads (Project Loom) are numerous, short-lived, and not reused. The ThreadLocal approach causes memory bloat, prevents reuse, and complicates cleanup. Jackson's own ThreadLocalPoolBase javadoc acknowledges this: "may not work well on platforms where threads are not long-living or reused (like Project Loom)." The core solution: decouple buffer pools from thread identity.

3. First Step: Pluggable RecyclerPool (2.16)

Jackson 2.16 introduced the RecyclerPool interface to abstract the hardcoded ThreadLocal logic:

public interface RecyclerPool<P extends RecyclerPool.WithPool<P>> extends Serializable {
    P acquirePooled();
    void releasePooled(P pooled);
}

Multiple implementations were provided: ThreadLocalPool: current default implementation ConcurrentDequePool: based on ConcurrentLinkedDeque, globally shared, lock-free LockFreePool: CAS-based lock-free linked list BoundedPool: bounded queue, retains up to N instances (default 100) NonRecyclingPool: no recycling, allocates new each time

Users can configure JsonFactory with a specific pool:

JsonFactory f = JsonFactory.builder()
    .recyclerPool(JsonRecyclerPools.sharedConcurrentDequePool())
    .build();

However, the default pool choice determines most users' experience, and Jackson's default selection underwent chaotic changes.

4. Default Pool Flip-Flopping

2.16 : default ThreadLocalPool 2.17.0 (2024-01) : aggressively switched to LockFreePool to reduce global contention

2024-04 : regression reported; LockFreePool caused unbounded memory growth under high concurrency (issue #1260)

2.17.1 : emergency rollback to ThreadLocalPool 2.18 : tried ConcurrentDequePool Final : 2.x reverted ConcurrentDequePool, back to ThreadLocalPool; 3.x kept ConcurrentDequePool as new default

The LockFreePool implementation reveals the flaw: both acquirePooled and releasePooled use compareAndSet with only 3 retries. Under high contention, acquire fails and creates new objects, while release later succeeds and returns them to the unbounded pool, causing accumulation.

@Override
public P acquirePooled() {
    for (int i = 0; i < 3; i++) {
        Node<P> currentHead = head.get();
        if (currentHead == null) {
            return createPooled();
        }
        if (head.compareAndSet(currentHead, currentHead.next)) {
            currentHead.next = null;
            return currentHead.value;
        }
    }
    return createPooled();
}

@Override
public void releasePooled(P pooled) {
    Node<P> newHead = new Node<>(pooled);
    for (int i = 0; i < 3; i++) {
        newHead.next = head.get();
        if (head.compareAndSet(newHead.next, newHead)) {
            return;
        }
    }
}

5. Third Step: LockFreePool Removed in 3.x

Jackson 3.x removed LockFreePool API and LockFreePoolBase implementation entirely, including javadoc references. The design (especially giving up retries) is harmful for buffer pools; keeping a leaky pool is worse than removing it. 3.x retains three base implementations: ThreadLocal, ConcurrentDeque, Bounded. The default for 3.x is ConcurrentDequePool, a sensible choice for virtual threads.

6. Fourth Step: Fixing TOCTOU in BufferRecycler (3.2)

A concurrency bug remained in BufferRecycler 's releaseCharBuffer and releaseByteBuffer. The methods intend to replace the slot's buffer only if the new buffer is larger, but the get-compare-set sequence is non-atomic (TOCTOU). In April 2026 (issue #1598 / PR #1599), Jackson fixed this using accumulateAndGet for atomic updates:

public void releaseCharBuffer(int ix, char[] buffer) {
    // 13-Jan-2024, tatu: [core#1186] Replace only if beneficial.
    // Use accumulateAndGet to atomically update the slot, avoiding the
    // TOCTOU race of a non-atomic get+conditional-set sequence.
    _charBuffers.accumulateAndGet(ix, buffer,
        (current, b) -> (current == null || b.length > current.length) ? b : current);
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ConcurrencyVirtual ThreadsJacksonThreadLocalProject LoomTOCTOUBuffer PoolingRecyclerPool
Code Archaeology
Written by

Code Archaeology

Learn from the past to understand the present

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.