Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。
PS:关于直接内存可以参考我的这篇文章…不清楚的同学一定要看,因为 Netty 就是通过直接内存实现的零拷贝。
如果使用传统的 JVM 堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才能写入Socket 中。JVM 堆内存的数据是不能直接写入 Socket 中的。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
可以看下 Netty 的读写源码,比如 read 源码 NioByteUnsafe.read():
顺着 do while 中的 allocHandle.allocate(allocator) 一路向下点,最终走到了 AbstractByteBufAllocator 的 ioBuffer() 方法中:
如果我们顺着上面的 directBuffer() 再点下去,会走到 PooledUnsafeDirectByteBuf#initMemoryAddress(),这个方法其实就很清楚,创建直接内存的关键是它的起始地址,而基于堆内存 Buffer(比如IntBuffer、LongBuffer等)核心的一个暂存数据的成员数组。
总结一下,相当于是每次循环读取一次消息,就通过 ByteBufferAllocator 的 ioBuffer 方法获取 ByteBuf 对象。当进行 Socket IO 读写的时候,为了避免直接内存->堆内存->直接内存
的两次拷贝,Netty 的 ByteBuf 分配器直接创建非堆内存,通过“零拷贝”来提升读写性能。
PS:这里的堆内存也可以理解成用户空间,直接内存理解成内核空间
另外,Netty 的文件传输采用了 transferTo() 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write() 方式导致的内存拷贝问题。
// DefaultFileRegion
public long transferTo(WritableByteChannel target, long position) throws IOException {
long count = this.count - position;
if (count < 0 || position < 0) {
throw new IllegalArgumentException(
"position out of range: " + position +" (expected: 0 - " + (this.count - 1) + ')');
}
if (count == 0) {
return 0L;
}
if (refCnt() == 0) {
throw new IllegalReferenceCountException(0);
}
// Call open to make sure fc is initialized. This is a no-oop if we called it before.
open();
// transferTo
long written = file.transferTo(this.position + position, count, target);
if (written > 0) {
transferred += written;
}
return written;
}
对于很多操作系统它直接将文件缓冲区的内容发送到目标 Channel 中,而不需要通过拷贝的方式,这是一种更加高效的传输方式,它实现了文件传输的“零拷贝”。