JDK-6963060 : sun.nio.ch.Util ByteBuffer pool is is not efficient for its common usecase.
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.nio
  • Affected Version: 6u10
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: linux
  • CPU: x86
  • Submitted: 2010-06-22
  • Updated: 2012-03-20
  • Resolved: 2010-07-31
Related Reports
Duplicate :  
Description
FULL PRODUCT VERSION :


ADDITIONAL OS VERSION INFORMATION :
All OSes

A DESCRIPTION OF THE PROBLEM :
The ByteBuffer pool in sun.nio.ch.Util is designed to allows up to 3 ByteBuffers to be removed and return in any combination.  While this is flexible it add the cost of removing the ByteBuffer from the pool (cheap) and adding it back into the pool (expensive as it creates a SoftReference)

However, it common use case is there is only one ByteBuffer at a time in a single threaded manner (The pool is Thread Local)

This means you only need a pool on 1 and it doesn't need to be removed or returned. No SOftReference needs to be created on a per call basis.



STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
See code below

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Objects only need on startup/warmup.
ACTUAL -
Generates a stream of objects.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class SocketReadsMain {
    public static void main(String... args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(0);
        SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", serverSocket.getLocalPort()));
        socket.configureBlocking(false);
        final Socket socket2 = serverSocket.accept();
        socket.finishConnect();
        final ByteBuffer buffer = ByteBuffer.allocate(1024);
        int count = 0;
        while(true)  {
            long start = Runtime.getRuntime().freeMemory();
            for(int i= 0; i < 100*1000;i++)
                socket.read(buffer);
            long used = start - Runtime.getRuntime().freeMemory();
            System.out.println("Average used: "+used/100/1000);
        }
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Update the code to the following. Note this is simpler and more efficient for the cases where only one ByteBuffer is needed at a time.  Need to check this is the case in all situations.

import sun.nio.ch.DirectBuffer;

import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;

class Util {
    // Per-thread soft cache a temporary direct buffer. Only one need at any one time, the buffer has the capacity of the largest one need recently.
    private static final ThreadLocal<SoftReference<ByteBuffer>> bufferPool = new ThreadLocal<SoftReference<ByteBuffer>>();

    static ByteBuffer getTemporaryDirectBuffer(int size) {
        // Grab a buffer if available
        SoftReference<ByteBuffer> ref = bufferPool.get();
        ByteBuffer buf = null;
        if (ref != null && (buf = ref.get()) != null && buf.capacity() >= size) {
            buf.rewind();
            buf.limit(size);
            return buf;
        }
        if (buf != null)
            // release memory
            ((DirectBuffer) buf).cleaner().clean();

        // create a new one
        ByteBuffer buffer = ByteBuffer.allocateDirect(size);
        // replace the smallest one.
        bufferPool.set(new SoftReference<ByteBuffer>(buffer));

        return buffer;
    }

    static void releaseTemporaryDirectBuffer(ByteBuffer buffer) {
        // nothing to do.
    }
}


SUPPORT :
YES

Comments
EVALUATION There are changes in the works for 6971825 that mean that at most one buffer will be cached per therad when scatter/gather aren't used.
31-07-2010