先说明几个概念:
- 零拷贝:此处的零拷贝指的是没有CPU拷贝,现在不少人觉得linux的senfile才是零拷贝,这不重要,只要把各种技术点的实现细节整明白就好了
- DMA拷贝:内存与IO设备之间的拷贝,不使用cpu,而是使用主板上独立的芯片完成拷贝操作 eg:磁盘<-->内存、网卡<-->内存
非直接缓冲区(ByteBuffer):
- 在jvm中开辟数据空间(用户空间);
- 每次的io操作都会有内核空间缓存和jvm缓存之间的copy操作
- 以网络IO往外读为例:JVM用户内存 通过CPU拷贝->内核缓冲buffer 通过DMA拷贝->网络协议通道中
直接缓冲区DirectByteBuffer(零拷贝):
- 在jvm越过jvm堆直接开辟内核空间(address= unsafe.allocateMemory(size) 开辟一个size大小的空间返回该空间的首地址)
- jvm中只存有数据的引用地址,为了内核安全该地址也不是真正的内核空间地址,而是会有一个中间的内存地址映射表,完成寻址
- 以网络IO往外读为例:内核区缓冲bufferDMA拷贝->网络协议通道中
- 场景:将创建的DirectByteBuffer中添加的内容写出到文件或网络
mmap内存映射MappedByteBuffer (零拷贝)【mmap这块查阅了不少资料,结果都不尽相同,一下为个人理解】:
- 直接将一个磁盘文件地址作为一个虚拟内存地址,操作这块地址时若发现未在物理内存中,则会触发缺页中断,将其加载至内核的物理内存中操作,二者会建立映射关系,操作物理内存的动作也会反映到虚拟内存地址
- MappedByteBuffer的实现类之所以是DirectByteBuffer,是为了使用DirectByteBuffer对堆外内存的操作函数
- 多用于对大文件的直接操作
- 场景:直接修改磁盘文件内容
说明一个概念:jvm若要把自身的数据传送到外界时(比如IO操作),数据要先送往内核,然后由内核在内核空间中完成相应IO操作。
理论上内核是能访问到所有的内存空间的,为什么不直接访问jvm堆进行操作呢?
答:因为JVM进行GC时SWT只是停止所有jvm的操作,而不能停止内核中的操作,若内核正在访问时GC进行垃圾回收,将这块数据进行了复制移动,就会出问题
DirectByteBuffer的堆外空间是如何实现GC管理的呢:他有一个属性sun.misc.Cleaner类型的,该种类型的属性在进行垃圾回收时会触发cleaner.clean(),其中的逻辑是执行cleaner.thunk.run(),thunk是个runable的类型属性,而directByteBuffer.cleaner初始化的thunk中的run方法中的逻辑有一句unsafe.freeMemory(address);实现了堆外内存的回收【回收方式注意和netty中的引用计数主动释放区分】
IO中的管道与管道直连的零拷贝:
- linux环境下文件io中的fileChannel.transferTo方法,底层调用的linux中独有的指令sendfile
- 以传送文件为例:磁盘DMA拷贝->内核缓冲bufferDMA拷贝->网络协议通道中(如果不是零拷贝过程:磁盘DMA拷贝->内核内存cpu拷贝->jvm内存cpu拷贝->内核缓冲bufferDMA拷贝->网络协议通道中)
- 场景:将两个通道直接连接传送数据,例如将文件发送到网络io,将网络io写入文件(channel.transferTo/transferFrom在linux的系统底层调用的就是sendfile指令)