关于java.nio.ByteBuffer的一些杂七杂八。

任何网络程序框架都会面临一个问题:如何提供一个高效的buffer?比如我们想写一个http server,那么就需要不断的从文件中读入数据,然后写入到socket中,如:

byte[] buf=new byte[4096];
while(file.read(buf)){
  mysocket.write(buf);
}

java.nio中引入了一个重要的类:ByteBuffer,来做这件事情。(我的直觉是它应该和ACE的MessageBlock作用很像,但是后来发现接口迥异。)
ByteBuffer是一个抽象类,它有两种实现:HeapByteBuffer 和 DirectByteBuffer。java.nio.ByteBuffer.allocate(int)返回的是HeapByteBuffer,java.nio.ByteBuffer.allocateDirect(int)返回的是DirectByteBuffer。
HeapByteBuffer分配在jvm的堆(如新生代)上,和其它对象一样,由gc来扫描、回收。DirectByteBuffer则是通过底层的JNI向C Runtime Time通过malloc分配,在JVM的GC所管理的堆之外。

下面讨论HeapByteBuffer。
每个HeapByteBuffer内部有一个byte[]存储数据。这个byte[]在构造HeapByteBuffer的时候分配好,长度不会自动增长。

HeapByteBuffer内部有四个指针(offset):
capacity:内部这个byte数组的大小(byte[]的length)。
mark:相当于书签,初始值为-1。需要设置的时候mark()一下,需要跳回去的时候用reset()方法。
position:指向下一个读取/写入位置。初始值为0,读/写 数据的时候自动往后挪这个指针。
limit:初始值等于 capacity。
它们始终满足这样的关系:mark <= position <= limit <= capacity。这4个指针经常把我绕的晕乎乎的。

flip操作:用在读写操作转换的时候。

limit = position;
position = 0; 
mark = -1; //清理掉书签

示例用法:

buf.put(magic);    // 先往buffer里面写入一个包头(packet header)
in.read(buf);      // 然后从另外一个input stream中读入包体,并写入到buffer中
buf.flip();        // Flip buffer。刚才是往ByteBuffer里写数据,下面要转换成读数据。
out.write(buf);    // 把整个buffer里的有效数据(包头+包体)读出来,写入output stream中。

但是调用这个方法之前一定要注意,不要多调用了一次。比如,把上面的第三行代码复制一遍,那么

buf.put(magic);    // 先往buffer里面写入一个包头(packet header)
in.read(buf);      // 然后从另外一个input stream中读入包体,并写入到buffer中
buf.flip();        // Flip buffer。position=0。
buf.flip();        // Flip buffer。limit=0!
out.write(buf);    // 什么也不会写入。

假如让你实现一个readfile这样的函数,你会在函数的末尾调用buf.flip吗?

void readfile(File f,ByteBuffer bb){
  f.read(bb);
  bb.flip(); //Do it or not do it ? That's a question。
}

你会在这个函数的接口注释那里说“我没调用flip!!!”吗?

ByteBuffer的toArray()?
有时候,想把ByteBuffer转成一个byte[],把它里面的有效数据拿出来。但是可惜它并没有一个toArray()这样的方法。于是就需要手动copy一下。

ByteBuffer bb;
byte[] contentsOnly = Arrays.copyOf( bb.array(), bb.position() );

DirectByteBuffer的接口和HeapByteBuffer完全一样,最大的区别就是内存位置不一样。如果有大量的文件需要以memory mapping的方式映射到内存中,那么DirectByteBuffer明显优于HeapByteBuffer。因为这部分内存不用被gc,所以降低了gc消耗。另外,HeapByteBuffer的缓存区无法作为操作系统direct io api的缓存区,因为它未必是按page size对齐的。

关于ByteBuffer的性能测试:
往ByteBuffer里面写Float:比较数组(new float[10000])、HeapByteBuffer、DirectByteBuffer的性能差距。

数组: 2.06 微秒
DirectByteBuffer:3.94 微秒
普通buffer: 16.88 微秒
测试环境:Java HotSpot(TM) 64-Bit Server VM (build 21.0-b17, mixed mode), windows 7 64bit。
测试代码:

private final FloatBuffer byteBuffer2 = ByteBuffer.allocate(40000).order(ByteOrder.nativeOrder())
        .asFloatBuffer();
private final FloatBuffer byteBuffer = ByteBuffer.allocateDirect(40000).order(ByteOrder.nativeOrder())
        .asFloatBuffer();

protected void testWrite() {
    // Allow VM to compile
    testWriteArray(1000);
    testWriteByteBuffer(1000);
    // Test for real
    System.out.println("Array buffer: " + testWriteArray(50000) + " us per iteration");
    System.out.println("Direct buffer: " + testWriteByteBuffer(50000) + " us per iteration");
}

protected double testWriteByteBuffer(int iterations) {
    long timeNow = System.currentTimeMillis();
    for (int j = 0; j < iterations; j++) {
        for (int i = 0; i < 10000; i++) {
            byteBuffer.put(i, 1234.5678f);
        }
    }
    return 1000.0 * (System.currentTimeMillis() - timeNow) / iterations;
}

protected double testWriteArray(int iterations) {
    long timeNow = System.currentTimeMillis();
    for (int j = 0; j < iterations; j++) {
        for (int i = 0; i < 10000; i++) {
            byteBuffer2.put(i, 1234.5678f);
        }
    }
    return 1000.0 * (System.currentTimeMillis() - timeNow) / iterations;
}

public BufferMark() {
}

public static void main(String[] args) {
    for (int n = 0; n < 10; n++) {
        System.out.println("=== round " + n);
        BufferMark app = new BufferMark();
        app.testWrite();
    }
}

此博客中的热门博文

少写代码,多读别人写的代码

在windows下使用llvm+clang

tensorflow distributed runtime初窥