I have a buffer pool implementation which basically provides pre-allocated ByteBuffer objects via allocate()
/release()
API. In order to detect the cases when caller forgot to call release and the ByteBuffer ref is leaked, I am using Guava's FinalizableReferenceQueue
in conjunction with FinalizablePhantomReference. finalizeReferent()
.
Additionally, I need to selectively destroy the buffer pool and replace it with a newer one with a different configuration. For that, I was setting previous SampleBufferPool
reference to null
and let garbage collector do its job. However, I noticed that the ByteBuffer were not getting collected/finalizeReferent is not being called. (I verified that the full GC pause are not collecting any memory via adding -XX:+PrintGCDetails -verbose:gc
JVM flags)
package foo;
import com.google.common.base.FinalizablePhantomReference;
import com.google.common.base.FinalizableReferenceQueue;
import com.google.common.collect.Sets;
import java.lang.ref.Reference;
import java.nio.ByteBuffer;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.stream.IntStream;
public class App {
public static class SampleBufferPool {
// Phantom reference queue for detecting memory leaks
// See. https://guava.dev/releases/19.0/api/docs/com/google/common/base/FinalizableReferenceQueue.html
private static final FinalizableReferenceQueue FRQ = new FinalizableReferenceQueue();
// This ensures that the FinalizablePhantomReference itself is not garbage-collected.
public static final Set<Reference<?>> REFERENCES = Sets.newConcurrentHashSet();
private final ConcurrentLinkedDeque<ByteBuffer> _bufferCache = new ConcurrentLinkedDeque<>();
private final int _chunkSize;
private final int _numChunks;
public SampleBufferPool(int chunkSize, int numChunks) {
_chunkSize = chunkSize;
_numChunks = numChunks;
IntStream.range(0, _numChunks).forEach(i -> populateSingleChunk());
}
public ByteBuffer allocate() {
return _bufferCache.pollLast();
}
public void release(ByteBuffer chunk) {
_bufferCache.offerLast(chunk);
}
private void populateSingleChunk() {
ByteBuffer chunk = ByteBuffer.allocate(_chunkSize);
_bufferCache.offerLast(chunk);
Reference<?> reference = new FinalizablePhantomReference<>(chunk, FRQ) {
@Override
public void finalizeReferent() {
REFERENCES.remove(this);
System.out.println("LEAK DETECTED. ByteBuf[" + "] from RecyclingMemoryPool");
}
};
REFERENCES.add(reference);
}
}
public static void main(String[] args) {
SampleBufferPool sampleBufferPool = new SampleBufferPool(20000000, 400);
sampleBufferPool = null;
for (int i = 0; i < 10; i++) {
System.gc();
}
}
}