IIRC I never published code that does ByteBuffer pooling? I think you refer to this code that does slice a large ByteBuffer into smaller ones (to avoid the 4K overhead per malloc). Once the large ByteBuffer is completely consumed (as in: you can’t slice off the demanded N bytes), it simply allocates another large ByteBuffer and starts slicing that. I let the GC figure out when all the sliced buffers are not referenced any longer, and the large ByteBuffer will automatically be deallocated.
If you however want true pooling, I advice you to make a List[], containing power-of-two sized ByteBuffers:
public class ByteBufferPool {
final List<ByteBuffer>[] potBuffers;
public ByteBufferPool()
{
potBuffers= (List<ByteBuffer>[]) new List[32];
for (int i = 0; i < potBuffers.length; i++) {
potBuffers[i] = new ArrayList<ByteBuffer>();
}
}
public ByteBuffer aquire(int bytes) {
int alloc = allocSize(bytes);
int index = Integer.numberOfTrailingZeros(alloc);
List<ByteBuffer> list = potBuffers[index];
ByteBuffer bb = list.isEmpty() ? create(alloc) : list.remove(list.size() - 1);
bb.position(0).limit(bytes);
// fill with zeroes to ensure deterministic behavior upon handling 'uninitialized' data
for (int i = 0, n = bb.remaining(); i < n; i++) {
bb.put(i, (byte) 0);
}
return bb;
}
public void release(ByteBuffer buffer) {
int alloc = allocSize(buffer.capacity());
if (buffer.capacity() != alloc) {
throw new IllegalArgumentException("buffer capacity not a power of two");
}
int index = Integer.numberOfTrailingZeros(alloc);
potBuffers[index].add(buffer);
}
public void flush() {
for (int i = 0; i < potBuffers.length; i++) {
potBuffers[i].clear();
}
}
private static int LARGE_SIZE = 1024 * 1024;
private ByteBuffer largeBuffer = malloc(LARGE_SIZE);
private ByteBuffer create(int bytes) {
if (bytes > LARGE_SIZE)
return malloc(bytes);
if (bytes > largeBuffer.remaining()) {
largeBuffer = malloc(LARGE_SIZE);
}
largeBuffer.limit(largeBuffer.position() + bytes);
ByteBuffer bb = largeBuffer.slice();
largeBuffer.position(largeBuffer.limit());
return bb;
}
private static ByteBuffer malloc(int bytes) {
return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder());
}
private static int allocSize(int bytes) {
if (bytes <= 0) {
throw new IllegalArgumentException("attempted to allocate zero bytes");
}
return (bytes > 1) ? Integer.highestOneBit(bytes - 1) << 1 : 1;
}
}
Code is completely untested, not even compiled.
Using POT buffer sizes solves a lot of problems. In case you worry about fragmentation, simply flush() the pool every once in a while, so that the GC can cleanup after you.
Threadsafety is easily obtained by either using ThreadLocals or extending the class in the example code and making all public methods synchronized. Both (more or less) hurt performance, so you might want to read up on lockfree datastructures. Keep in mind though that it’s unlikely that it will be your bottleneck, unless you aquire/release in some inner loop.
public static ByteBufferPool synced(final Object mutex) {
if (mutex == null) {
throw new NullPointerException();
}
return new ByteBufferPool() {
@Override
public ByteBuffer aquire(int bytes) {
synchronized (mutex) {
return super.aquire(bytes);
}
}
@Override
public void release(ByteBuffer buffer) {
synchronized (mutex) {
super.release(buffer);
}
}
@Override
public void flush() {
synchronized (mutex) {
super.flush();
}
}
};
}