有一个很常见的场景,好比须要将文件从磁盘上原封不动地发送到网络的另外一端。这经过代码实现起来很简单:对于Java而言,你可使用InputStream
的某个实现类将文件一块块地读取到小的缓冲区(一般咱们都将缓冲区大小设置为8KB),而后再将缓冲区数据输出到OutputStream
中。更好的作法是你能够建立一个PipedInputStream
实例,让它来管理缓冲区。可是,若是你的应用对性能有要求,那么经过这种方式去读取文件再发送在操做系统层面来看就显得有些太耗资源了。
为何这样说呢?结合下图我来解释下缘由html
read()
系统调用;read()
调用返回;write()
系统调用;若是你的应用不关心延时和吞吐量等性能指标,那么以上作法是没问题的,可是若是你的应用有这方面要求,好比静态资源服务器,那么这样作将会没法知足性能要求。上图中有4次上下文切换以及2次没必要要的内存拷贝。java
从上面的方式中能够很清楚的看到,将数据从内核缓冲区拷贝到应用缓冲区,以及从应用缓冲区拷贝到socket内核缓冲区是彻底不必的,由于咱们没有对数据做任何处理,仅仅只是将数据从一个socket倒腾到另外一个socket。零拷贝技术就能消除这两次额外的内存拷贝。零拷贝技术的实现方式没有一个统一的标准,它取决于不一样的操做系统。典型地,那些UNIX LIKE系统用sendfile()
来实现零拷贝功能。
使用零拷贝方式实现上面场景的图示以下linux
你可能会说,操做系统仍是要在内核内存空间作一次拷贝呀!是的。可是从操做系统的角度来讲,它已是零拷贝了,由于已经没有数据从内核空间拷贝到用户空间了。内核须要作一次拷贝的缘由是通常的硬件DMA方式只能存取连续的内存空间(因此才有了缓冲区)。可是若是硬件支持scatter-n-gather特性,此次的拷贝就能够避免。
支持scatter-n-gather特性时的图示以下缓存
不少WEB服务器都支持零拷贝,好比Tomcat和Apache。默认状况下Apache的这个特性是关闭的。
注意: Java的NIO经过transferTo
方法提供零拷贝。服务器
上面的零拷贝方案有个问题,由于没有涉及到用户态,因此除了打通流管道,咱们没法经过代码来修改流管道里的数据。不过如今有个比零拷贝昂贵但优于传统I/O的方案——内存映射,简称MMap网络
MMap容许代码将文件映射到内核内存,应用能够直接访问这个内核内存,就像访问用户态的内存空间同样,这样就不会产生内核空间到用户空间的内存拷贝。不过这种方式仍然须要4次上下文切换以及3次数据拷贝(其中有一次是CPU参与的内核内存拷贝)。操做系统将文件的某块数据映射到内存,受益于操做系统的虚拟内存管理,热点数据能被提早载入到内存,全部的数据是页对齐的,所以不须要缓冲区拷贝就能将数据倒腾到目标socket。app
虽然,MMap避免了额外的内存拷贝,可是使用了MMap不必定会比普通的方式快,这取决于不一样的操做系统。由于这涉及到MMap的建立和销毁所须要的性能开销以及页缺失时的负面影响。socket
Java中实现MMap方式的类是MappedByteBuffer
,它其实也是一种DirectByteBuffer
(DirectByteBuffer
是MappedByteBuffer
的子类),不过这两个类并无直接的关系。咱们一般所说的直接内存并无MMap的特性。性能
Java NIO中有三种ByteBuffer
优化
HeapByteBuffer
:ByteBuffer.allocate()
使用的就是这种缓冲区,叫堆缓冲区,由于它是在JVM堆内存的,支持GC和缓存优化。可是它不是页对齐的,也就是说若是要使用JNI的方式调用native代码时,JVM会先将它拷贝到页对齐的缓冲空间。DirectByteBuffer
:ByteBuffer.allocateDirect()
方法被调用时,JVM使用C语言的malloc()
方法分配堆外内存。因为不受JVM管理,这个内存空间是页对齐的且不支持GC,和native代码交互频繁时使用这种缓冲区能提升性能。不过内存分配和销毁的事就要靠你本身了。MappedByteBuffer
:FileChannel.map()
调用返回的就是这种缓冲区,这种缓冲区用的也是堆外内存,本质上其实就是对系统调用mmap()
的封装,以便经过代码直接操纵映射物理内存数据。