在Java中,您可以使用InputStream逐块读取数据到一个小的数据块(通常为8KB)中,然后将其写入到OutputStream。
如果您的软件要求低延迟对的话,那么从OS的角度来看这代价会非常昂贵。
这篇文章先讲一下传统这种方式IO方式会有些缺点,作为一个话题引子,引出要讨论的零拷贝技术。
在这样的情况下会发生什么
好,这是使用上述代码时发生的事情:
- JVM发起read()系统调用。
- OS操作系统切换到内核模式,将数据读入socket buffer。
- 然后,OS内核请求硬件设备获取数据,DMA将数据复制到内核空间的kernel buffer中,然后CPU将内核中的数据复制到用户空间user buffer中,切换回用户模式。read()调用返回。--读数据过程
- JVM处理代码逻辑然后发起write()系统调用。--写数据过程
- 操作系统切换到内核模式,将数据从用户user buffer复制到输出sockek buffer中。
- 操作系统返回到用户模式,JVM逻辑继续进行。
如果上面的示例有4次上下文切换和2个额外的复制过程-低效。
操作系统等级的零副本进行优化
显然,在这个用例中,完全不需要来回复制数据,因为除了将数据转储到其他之外,我们没有做任何其他事情。因此,此处可以使用零拷贝来优化。来看下sendfile()这种方式的零拷贝过程。如下所示:
您的可能会说OS仍必须在内核内存空间中复制数据。是的,但是从OS的角度来看,这已经是零拷贝了,因为没有数据从内核空间复制到用户空间。内核需要进行复制的原因是,因为硬盘的每次读写都会牵涉到DMA的过程,而文件系统对硬盘的I / O请求不是连续的,数据所在的物理内存页面也是不连续的,如果硬件支持scatter-n-gather ,则可以避免这种情况:
许多Web服务器确实支持零复制,例如Tomcat和Apache。例如,可以在这里找到apache的相关文档,但通常处于关闭状态。
注意:Java的NIO通过transferTo(doc)提供此功能。
mmap
上面的零复制方法的问题在于,因为实际上没有涉及用户模式,代码无法执行其他任何操作。但是有一种更昂贵但更有用的方法-mmap,内存映射。
Mmap支持代码将文件映射到内核内存,并在应用程序中用户空间中一样直接访问文件,从而避免了其中的重复的复制。作为权衡,这仍将涉及4个交替切换。但是,由于OS将某些文件映射到内存中,因此您可以从OS虚拟内存管理中获得所有好处-可以智能地有效地缓存内容,并且所有数据都是页面对齐的,因此不需要复制就可以将内容写回。
NIO DirectByteBuffer
Java NIO了ByteBuffer,它用于通道的buffer区域。共有3种主要实现:
1. HeapByteBuffer
在ByteBuffer.allocate()调用时使用。之所以称为堆,是因为它是在JVM的堆空间中维护的,因此您获得所有好处,例如GC支持和缓存优化。但是,它不是页面对齐的,这意味着如果您需要通过JNI调用本地代码的话,则JVM必须将副本复制到对齐的对齐空间中。
摘要
sendfile()和mmap()为多个多个数据操作提供高效,低延迟的低级解决方案。同样,没有人认为这些是银弹,因为现实世界中的场景可能很复杂,如果这在大多数情况下下,要使软件工程获得大的投资回报,好先“做到正确”,然后再“做到快速”。
原文引用:https://medium.com/@xunnan.xu/its-all-about-buffers-zero-copy-mmap-and-java-nio-50f2a1bfc05c