BufferRecycler.java
package tools.jackson.core.util;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Objects;
/**
* This is a small utility class, whose main functionality is to allow
* simple reuse of raw byte/char buffers. It is usually allocated through
* {@link RecyclerPool} (starting with 2.16): multiple pool
* implementations exists.
* The default pool implementation uses
* {@code ThreadLocal} combined with {@code SoftReference}.
* The end result is a low-overhead GC-cleanable recycling: hopefully
* ideal for use by stream readers.
*<p>
* Rewritten in 2.10 to be thread-safe (see [jackson-core#479] for details),
* to not rely on {@code ThreadLocal} access.<br>
* Rewritten in 2.16 to work with {@link RecyclerPool} abstraction.
*/
public class BufferRecycler
implements RecyclerPool.WithPool<BufferRecycler>
{
/**
* Tag-on interface to allow various other types to expose {@link BufferRecycler}
* they are constructed with.
*
* @since 2.17
*/
public interface Gettable {
/**
* @return Buffer recycler instance object is configured with, if any;
* whether this can be {@code null} depends on type of object
*/
public BufferRecycler bufferRecycler();
}
/**
* Buffer used for reading byte-based input.
*/
public final static int BYTE_READ_IO_BUFFER = 0;
/**
* Buffer used for temporarily storing encoded content; used
* for example by UTF-8 encoding writer
*/
public final static int BYTE_WRITE_ENCODING_BUFFER = 1;
/**
* Buffer used for temporarily concatenating output; used for
* example when requesting output as byte array.
*/
public final static int BYTE_WRITE_CONCAT_BUFFER = 2;
/**
* Buffer used for concatenating binary data that is either being
* encoded as base64 output, or decoded from base64 input.
*/
public final static int BYTE_BASE64_CODEC_BUFFER = 3;
/**
* Buffer used as input buffer for tokenization for character-based parsers.
*/
public final static int CHAR_TOKEN_BUFFER = 0;
/**
* Buffer used by generators; for byte-backed generators for buffering of
* {@link String} values to output (before encoding into UTF-8),
* and for char-backed generators as actual concatenation buffer.
*/
public final static int CHAR_CONCAT_BUFFER = 1;
/**
* Used through {@link TextBuffer}: directly by parsers (to concatenate
* String values)
* and indirectly via
* {@link tools.jackson.core.io.SegmentedStringWriter}
* when serializing (databind level {@code ObjectMapper} and
* {@code ObjectWriter}). In both cases used as segments (and not for whole value),
* but may result in retention of larger chunks for big content
* (long text values during parsing; bigger output documents for generation).
*/
public final static int CHAR_TEXT_BUFFER = 2;
/**
* For parsers, temporary buffer into which {@code char[]} for names is copied
* when requested as such; for {@code WriterBasedGenerator} used for buffering
* during {@code writeString(Reader)} operation (not commonly used).
*/
public final static int CHAR_NAME_COPY_BUFFER = 3;
// Buffer lengths
// 22-Jun-2026, [core#1622]: bumped default base64 codec buffer (index
// BYTE_BASE64_CODEC_BUFFER) from 2000 to 4000 to reduce InputStream
// reads when encoding binary content of unknown/large length.
private final static int[] BYTE_BUFFER_LENGTHS = new int[] { 8000, 8000, 2000, 4000 };
private final static int[] CHAR_BUFFER_LENGTHS = new int[] { 4000, 4000, 200, 200 };
/**
* Number of unused slots at each end of {@link #_slots}.
* A pool keeps a recycler for a long time, and each use of the recycler
* writes its slots. Without padding, these slots share cache lines with
* other objects that other threads read, such as serializers and the
* values that they write. Each write then takes those lines away from
* the other cores (false sharing). 32 references are 128 bytes with
* compressed oops (256 bytes without), the default padding of the JDK's
* {@code @Contended}. This covers 128-byte cache lines (as on Apple
* silicon) and the x86 adjacent-line prefetcher, which fetches 64-byte
* lines in pairs.
*/
private final static int SLOT_PADDING = 32;
private final static VarHandle SLOTS = MethodHandles.arrayElementVarHandle(Object[].class);
/**
* Pool link, then the byte buffers, then the char buffers, with
* {@link #SLOT_PADDING} unused slots before and after them
* (none if not padded, see constructor).
*/
private final Object[] _slots;
/**
* Index of the pool link in {@link #_slots}.
*/
private final int _poolSlot;
/**
* Index of the first byte buffer in {@link #_slots}.
*/
private final int _byteSlotBase;
/**
* Index of the first char buffer in {@link #_slots}.
*/
private final int _charSlotBase;
private final int _byteBufferCount;
private final int _charBufferCount;
/*
/***********************************************************************
/* Life-cycle
/***********************************************************************
*/
/**
* Default constructor used for creating instances of this default
* implementation.
*/
public BufferRecycler() {
this(true);
}
/**
* @param padded Whether to pad slots; see {@link #BufferRecycler(int, int, boolean)}
*
* @since 3.3
*/
protected BufferRecycler(boolean padded) {
this(4, 4, padded);
}
/**
* Alternate constructor to be used by sub-classes, to allow customization
* of number of low-level buffers in use.
*
* @param bbCount Number of {@code byte[]} buffers to allocate
* @param cbCount Number of {@code char[]} buffers to allocate
*/
protected BufferRecycler(int bbCount, int cbCount) {
this(bbCount, cbCount, true);
}
/**
* Alternate constructor to be used by sub-classes, to allow customization
* of number of low-level buffers in use, and of slot padding.
*
* @param bbCount Number of {@code byte[]} buffers to allocate
* @param cbCount Number of {@code char[]} buffers to allocate
* @param padded Whether to pad slots against false sharing: only useful
* for instances that are kept and reused, not for one-off ones.
*
* @since 3.3
*/
protected BufferRecycler(int bbCount, int cbCount, boolean padded) {
if (bbCount < 0 || cbCount < 0) {
throw new IllegalArgumentException("Negative buffer count: bbCount="+bbCount+", cbCount="+cbCount);
}
final int padding = padded ? SLOT_PADDING : 0;
_poolSlot = padding;
_byteSlotBase = padding + 1;
_charSlotBase = _byteSlotBase + bbCount;
_slots = new Object[padding + 1 + bbCount + cbCount + padding];
_byteBufferCount = bbCount;
_charBufferCount = cbCount;
}
/**
* @return True if this recycler is linked to pool and may be released
* with {@link #releaseToPool()}; false if no linkage exists.
*
* @since 2.17
*/
public boolean isLinkedWithPool() {
return _slots[_poolSlot] != null;
}
/*
/***********************************************************************
/* Public API, byte buffers
/***********************************************************************
*/
/**
* @param ix One of <code>READ_IO_BUFFER</code> constants.
*
* @return Buffer allocated (possibly recycled)
*/
public final byte[] allocByteBuffer(int ix) {
return allocByteBuffer(ix, 0);
}
public byte[] allocByteBuffer(int ix, int minSize) {
final int DEF_SIZE = byteBufferLength(ix);
if (minSize < DEF_SIZE) {
minSize = DEF_SIZE;
}
// Object-typed arguments and result match the VarHandle access mode exactly
final Object old = SLOTS.getAndSet(_slots, _byteSlot(ix), (Object) null);
byte[] buffer = (byte[]) old;
if (buffer == null || buffer.length < minSize) {
buffer = balloc(minSize);
}
return buffer;
}
public void releaseByteBuffer(int ix, byte[] buffer) {
// 13-Jan-2024, tatu: [core#1186] Replace only if beneficial.
// [core#1598] Update the slot atomically, avoiding the TOCTOU race
// of a non-atomic get+conditional-set sequence.
final int slot = _byteSlot(ix);
Object current;
do {
current = SLOTS.getVolatile(_slots, slot);
if (current != null && ((byte[]) current).length >= buffer.length) {
return;
}
} while (!SLOTS.compareAndSet(_slots, slot, current, (Object) buffer));
}
/*
/***********************************************************************
/* Public API, char buffers
/***********************************************************************
*/
public final char[] allocCharBuffer(int ix) {
return allocCharBuffer(ix, 0);
}
public char[] allocCharBuffer(int ix, int minSize) {
final int DEF_SIZE = charBufferLength(ix);
if (minSize < DEF_SIZE) {
minSize = DEF_SIZE;
}
final Object old = SLOTS.getAndSet(_slots, _charSlot(ix), (Object) null);
char[] buffer = (char[]) old;
if (buffer == null || buffer.length < minSize) {
buffer = calloc(minSize);
}
return buffer;
}
public void releaseCharBuffer(int ix, char[] buffer) {
// 13-Jan-2024, tatu: [core#1186] Replace only if beneficial.
// [core#1598] Update the slot atomically, avoiding the TOCTOU race
// of a non-atomic get+conditional-set sequence.
final int slot = _charSlot(ix);
Object current;
do {
current = SLOTS.getVolatile(_slots, slot);
if (current != null && ((char[]) current).length >= buffer.length) {
return;
}
} while (!SLOTS.compareAndSet(_slots, slot, current, (Object) buffer));
}
private int _byteSlot(int ix) {
return _byteSlotBase + Objects.checkIndex(ix, _byteBufferCount);
}
private int _charSlot(int ix) {
return _charSlotBase + Objects.checkIndex(ix, _charBufferCount);
}
/*
/***********************************************************************
/* Overridable helper methods
/***********************************************************************
*/
protected int byteBufferLength(int ix) {
return BYTE_BUFFER_LENGTHS[ix];
}
protected int charBufferLength(int ix) {
return CHAR_BUFFER_LENGTHS[ix];
}
/*
/***********************************************************************
/* Actual allocations separated for easier debugging/profiling
/***********************************************************************
*/
protected byte[] balloc(int size) { return new byte[size]; }
protected char[] calloc(int size) { return new char[size]; }
/*
/***********************************************************************
/* WithPool implementation
/***********************************************************************
*/
/**
* Method called by owner of this recycler instance, to provide reference to
* {@link RecyclerPool} into which instance is to be released (if any)
*
* @since 2.16
*/
@Override
public BufferRecycler withPool(RecyclerPool<BufferRecycler> pool) {
if (_slots[_poolSlot] != null) {
throw new IllegalStateException("BufferRecycler already linked to pool: "+pool);
}
// assign to pool to which this BufferRecycler belongs in order to release it
// to the same pool when the work will be completed
_slots[_poolSlot] = Objects.requireNonNull(pool);
return this;
}
/**
* Method called when owner of this recycler no longer wishes use it; this should
* return it to pool passed via {@code withPool()} (if any).
*
* @since 2.16
*/
@Override
@SuppressWarnings("unchecked")
public void releaseToPool() {
RecyclerPool<BufferRecycler> tmpPool = (RecyclerPool<BufferRecycler>) _slots[_poolSlot];
if (tmpPool != null) {
// nullify the reference to the pool in order to avoid the risk of releasing
// the same BufferRecycler more than once, thus compromising the pool integrity
_slots[_poolSlot] = null;
tmpPool.releasePooled(this);
}
}
}