零拷贝、mmap 和ByteBuffer

1、传统IO底层发生了什么?为什么OS层面是相当昂贵的操作?

image.png

  1. JVM发送read()系统调用
  2. OS进行上下文切换到内核态,从socket buffer中读取数据。
  3. OS内核拷贝数据到user buffer,然后进行上下文切换,回到用户态,read()方法返回。
  4. JVM进程继续执行代码逻辑,然后发送write()系统调用。
  5. OS进行上下文切换到内核态,将user buffer的数据拷贝到socket buffer。
  6. OS进行上下文切换到用户态,write方法返回,JVM继续执行代码逻辑。

可以看到,一个简单的传统IO操作,涉及了4次上下文切换和2次数据拷贝,所以说这个效率是相当低下的。如果在并发要求极高的中间件系统中,这样的效率是无法容忍的。

2、JDK是如何优化传统的IO操作,实现zero-copy效果的?

zero-copy的意思是不会将数据从内核态拷贝到用户态,而非不进行数据拷贝。
image.png

之所以内核态还会发生数据拷贝,是因为DMA(Direct Memory Access)希望访问的是一段连续的内存空间。

3、MMAP

zero-copy,因为没有发生用户态的调用,所以我们的代码不能做除了管道流(stream piping)之外的事情。但是有一种比zero-copy更友好,也更昂贵的方式,短暂的内存映射「mmap」。

image.png

mmap允许直接将文件映射到内存(映射的是文件地址,而非文件本身),在用户态就可以直接访问,避免了不必要的数据拷贝,但是这种方式仍然无法避免上下文切换。

另外OS直接将文件映射到了内存,所以mmap的方式可以获得OS虚拟内存管理的所有优势(依赖底层操作系统)。

  • 热点数据智能缓存,还会预读取相邻的data page。
  • 数据都存在于一段连续的内存空间中,无需在各种buffer间来回拷贝。

JavaNIO包下的MappedByteBuffer类就可以实现「mmap」,他是DirectByteBuffer 的变种。

4、ByteBuffer

Java NIO引入了ByteBuffer作为channel的缓冲区,ByteBuffer有三种主要实现:

  • HeadByteBuffer
  • DirectByteBuffer
  • MappedByteBuffer

HeadByteBuffer

使用ByteBuffer.allocate()创建,该ByteBuffer存在于堆空间,因此获得了GC的支持(可以被垃圾回收掉)以及进行了缓存的优化。但是他不是一段连续的内存空间,也就意味着如果你通过JNI的方式访问native代码,JVM会先拷贝到对其的buffer空间中。

DirectByteBuffer

使用ByteBuffer.allocateDirect()创建,JVM将会使用malloc()函数,分配堆空间之外的内存空间。好处是分配的内存空间是连续的,坏处是没有被JVM管理,这意味着你需要小心内存泄漏。

MappedByteBuffer

使用 FileChannel.map() 映射,分配堆空间之外的内存空间。本质上就是围绕mmap()的系统调用,让我们的java代码可以直接操纵映射的内存数据。

5、总结

虽然sendfile()mmap()提供了高效和低延迟。但需要注意的是软件技术没有银弹,带来高效的同时必将带来些许问题。在复杂的实际情况中,需认真考虑技术的引入是否值得(tradeoff)。脱离了JVM的管理和保护,代码复杂度的提升,更容易让软件崩溃(我的意思是崩溃,而非异常)。

Reference

It’s all about buffers: zero-copy, mmap and Java NIO

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享