本文来分享Netty中的零拷贝机制以及内存缓冲区ByteBuf的实现。
源码剖析基于Netty 4.1.52
Netty中的零拷贝
Netty中零拷贝机制次要有以下几种
1.文件传输类DefaultFileRegion#transferTo,调用FileChannel#transferTo,间接将文件缓冲区的数据发送到指标Channel,缩小用户缓冲区的拷贝(通过linux的sendfile函数)。
应用read 和 write过程如下
应用sendfile
能够看到,应用sendfile函数能够缩小数据拷贝以及用户态,内核态的切换
可参考: 操作系统和Web服务器那点事儿
2.Netty中提供了一些操作内存缓冲区的办法,如
Unpooled#wrappedBuffer办法,将byte数据,(jvm)ByteBuffer转换为ByteBuf
CompositeByteBuf#addComponents办法,合并ByteBuf
ByteBuf#slice办法,提取ByteBuf中局部数据片段
ByteBuf#duplicate,复制一个内存缓冲区
这些办法都是基于对象援用的操作,并没有内存拷贝,而是内存共享
3.应用堆外内存(jvm)ByteBuffer对Socket读写
如果应用JVM的堆内存读取Socket数据,JVM会将Socket数据读取到间接内存,再拷贝一份到堆内存中,写入数据到Socket也须要将堆内存拷贝一份到间接内存中,而后才写入Socket中。
因为操作系统进行io操作须要一个稳固的间断空间的字节空间, 然而java堆上的字节空间会随着gc进行而进行挪动, 如果操作系统读取堆上的空间, 就会出错。
应用堆外内存能够防止该拷贝操作。
留神,这里从内核缓冲区拷贝到用户缓冲区的操作并不能省略,毕竟咱们须要对数据进行操作,所以还是要拷贝到用户态的。
可参考:
知乎--Java NIO中,对于DirectBuffer,HeapBuffer的疑难
知乎--Java NIO direct buffer的劣势在哪儿?
ByteBuf
ByteBuf是用于与Channel交互的内存缓冲区,提供程序拜访和随机拜访。
Netty4中将ByteBuf调整为抽象类,从而晋升吞吐量。
1.ByteBuffer
先理解一下ByteBuffer,ByteBuffer是JVM提供的字节内存缓冲区。ByteBuf是在ByteBuffer上进行的扩大,底层还是应用ByteBuffer。
ByteBuffer有两个子类,DirectByteBuffer和HeapByteBuffer。
HeapByteBuffer应用ByteBuffer#hb(byte[])存储数据。
DirectByteBuffer是堆外内存,应用的是操作系统的间接内存,它保护了一个援用address指向了底层数据,从而操作数据。(并没有应用ByteBuffer#buff)
Buffer外围属性
int position; //以后操作地位。int mark; //为某一读过的地位做标记,便于某些时候回退到该地位。int capacity; //初始化时候的容量。int limit; // 读写的限度地位,读写超出该地位会报错
读写操作都是基于position,并以limit为限度的。mark,position,limit,capacity关系如下
0 <= mark <= position <= limit <= capacity
ByteBuffer提供了如下办法调整这些标记地位:
- clear
limit = position = 0
个别在把数据写入Buffer前调用
- flip
limit = position
position = 0
个别在从Buffer读出数据前调用
- rewind
position=0
limit不变
个别在把数据重写入Buffer前调用。
- compacting
革除曾经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的前面
ByteBuffer还提供了一些操作缓冲区的办法
- duplicate
创立新字节缓冲区,共享以后缓冲区内容
- slice
创立新字节缓冲区,共享以后缓冲区内容子序列。
Netty的ByteBuf应用readerIndex标记读地位,writerIndex标记写地位,比(jvm)ByteBuffer设计更优雅。
+-------------------+------------------+------------------+ | discardable bytes | readable bytes | writable bytes | | | (CONTENT) | | +-------------------+------------------+------------------+ | | | | 0 <= readerIndex <= writerIndex <= capacity
ByteBuf提供readerIndex/writerIndex等办法获取或设置这两个值,十分直观。另外,ByteBuf提供了如下办法操作缓冲区
- discardReadBytes
革除曾经读过的数据。未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的前面
- duplicate
创立新字节缓冲区,共享以后缓冲区内容
- slice(int index, int length)
创立共享内存的ByteBuf,从index开始,长度为length
- readSlice(int length)
创立共享内存的ByteBuf,从readerIndex开始,长度为length
- retainedDuplicate()
创立共享内存的ByteBuf,并且以后ByteBuf的援用计数加1
2.接口关系
AbstractByteBuf:实现一些公共逻辑,如读写前查看地位。
AbstractReferenceCountedByteBuf,增加援用计数逻辑,实现援用计数回收间接内存。
PooledByteBuf:实现池化ByteBuf的公共逻辑。对于Netty中的内存池前面有文章解析。
PooledByteBuf#memory是底层的内存存储,PooledDirectByteBuf该字段是ByteBuffer,PooledHeapByteBuf则是byte[]。
上面能够分为Unsafe,No_Unsafe两个维度。Unsafe就是sun.misc.Unsafe。
应用Unsafe能够进步性能,但Unsafe是JDK外部的类,并非公开规范,不肯定所有JDK都存在这个类, JDK当前也有可能去掉这个类,所以Netty提供了两套实现。
3.内存调配
前面有文章解析Netty内存池,分享Netty中如何分配内存给ByteBuf。这里先不深刻。
4.读写过程
上面看一下ByteBuf与Channel如何交互数据。
后面分享Netty读写过程的文章说过了,NioByteUnsafe#read办法读取数据。
NioByteUnsafe#read -> NioSocketChannel#doReadBytes -> AbstractByteBuf#writeBytes -> PooledByteBuf#setBytes
public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException { try { return in.read(internalNioBuffer(index, length)); } catch (ClosedChannelException ignored) { return -1; }}
index参数就是writerIndex,internalNioBuffer办法会结构一个新的ByteBuffer,并设置ByteBuffer#position为index
间接调用ReadableByteChannel#read读取数据
在《ChannelOutboundBuffer与flush操作》中曾经分享过,
ChannelOutboundBuffer#nioBuffers也是通过internalNioBuffer办法生成ByteBuffer,
作为参数调用NioSocketChannel#doWrite办法,间接将数据拷贝到Channel。
ByteBuf#internalNioBuffer -> PooledByteBuf#_internalNioBuffer
final ByteBuffer _internalNioBuffer(int index, int length, boolean duplicate) { index = idx(index); ByteBuffer buffer = duplicate ? newInternalNioBuffer(memory) : internalNioBuffer(); buffer.limit(index + length).position(index); return buffer; }
newInternalNioBuffer由子类实现,构建对应的DirectByteBuffer或者HeapByteBuffer,留神,这里的内存是共享的。
5.援用计数
因为应用了间接内存,不能依赖JVM垃圾回收器开释内存,Netty应用援用计数算法开释内存。
ReferenceCounted接口,代表须要显式开释的援用计数对象,retain办法减少援用计数,release办法缩小援用计数。
AbstractReferenceCountedByteBuf实现了ReferenceCounted接口,它保护了refCnt变量作为援用计数。
结构一个AbstractReferenceCountedByteBuf时,refCnt为1。
当援用计数release到0时,调用deallocate()办法开释内存。
PooledByteBuf#deallocate
protected final void deallocate() { if (handle >= 0) { final long handle = this.handle; this.handle = -1; memory = null; tmpNioBuf = null; chunk.arena.free(chunk, handle, maxLength, cache); chunk = null; recycle(); }}
这里调用的是PoolArena#free。
PoolArena能够了解为一个内存池,这里free理论是将内寄存回内存池中,由内存池决定是否须要销毁底层间接内存。
PoolArena前面有对应文章解析。
6.内存销毁
销毁DirectByteBuf,有两个形式
利用反射获取Unsafe,调用Unsafe#freeMemory
利用反射获取DirectByteBuffer#cleaner(sun.misc.Cleaner),通过反射调用cleaner#clean办法
因为Netty不确认JDK中是否存在sun.misc.Cleaner,所以它也实现了两套机制。
PoolArenaDirect#free -> Arena#destroyChunk
protected void destroyChunk(PoolChunk<ByteBuffer> chunk) { if (PlatformDependent.useDirectBufferNoCleaner()) { PlatformDependent.freeDirectNoCleaner(chunk.memory); } else { PlatformDependent.freeDirectBuffer(chunk.memory); }}
从PlatformDependent中确认是否应用CLEANER
if (maxDirectMemory == 0 || !hasUnsafe() || !PlatformDependent0.hasDirectBufferNoCleanerConstructor()) { USE_DIRECT_BUFFER_NO_CLEANER = false; DIRECT_MEMORY_COUNTER = null;}
满足以下条件中一个就应用CLEANER,否则应用NO_CLEANER
- 没有应用间接内存
- JVM不反对Unsafe
- ByteBuffer不存在无Cleaner的构造函数