本文基于 jdk8
在了解 NIO 前,先来看下 IO。
Java IO
原理探究
下面这个例子将数据从一个文件读取到后,写入到另一个文件。
FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(des);
byte[] bytes = new byte[1024 * 8];
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
这里的 fis.read函数调用的是:
跟进 OpenJdk 的源码,可以看到 readBytes 的实现,如下图:
中间经过了三次数据 copy
- DMA 从硬件 copy 到内核空间
- 内核空间 copy 到堆外内存 buf (用户空间)
- 从 buf copy 到 JVM 堆内存 byte数组
这里有个问题,为什么中间多一步从内核空间 copy 到堆外内存 buf,不直接从内核空间 copy 到 JVM 堆内存呢?这是因为 JVM 有垃圾回收机制,内存地址可能会发送变化,需要 native 代码将 JVM 数组引用转换为实际的内存地址。
交互过程如下图:
优缺点、适用场景
优点就是简单直观,易懂易上手。缺点主要是性能问题,因为是阻塞 IO,而 IO 又是非常慢的操作,会浪费大量进程、线程资源。
数据量不大,并发不高,对阻塞不在意。
JAVA NIO
1.4引入的,提供了比传统 IO 更高效、更灵活的 IO 操作。
NIIO 主要接口
- Buffer:顾名思义,就是一块可读、可写的内存区。
- Channel:读写数据的双向通道,面向 Buffer,非阻塞式。主要类型有面向网络的 SocketChannel、ServerSocketChannel、DatagramChannel,还有面向文件的 FileChannel。
- Selector:多路复用选择器,封装了下 IO 多路复用中的 select,参见 如何理解Linux IO 模型 (中的多路复用 )
Buffer
通过三个例子来看下不同的 buffer。
HeapByteBuffer
FileChannel readChannel = fis.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 8);
readChannel.read(byteBuffer);
可以看到这里分配的是 HeapByteBuffer
跟进源码
如下图所示,这里分配了一个临时 DirectBuffer
继续跟代码,发现后边就是系统调用了。交互过程类似上述 IO,如下图所示:
DirectByteBuffer
FileChannel readChannel = fis.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 8);
readChannel.read(byteBuffer);
交互过程如下图,从图上可以看出,比 HeapByteBuffer 减少了一次用户态的数据copy。
MappedByteBuffer
DirectByteBuffer 依然有两次数据 copy,一次是从硬件设备 copy 到内核空间,一次是从内核空间 copy 到用户空间。那有没有更高效的方式呢?来一起看下 MappedByteBuffer。
FileChannel channel = FileChannel.open(Paths.get("xxx.txt"), StandardOpenOption.READ);
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_ONLY
, 0, channel.size());
交互过程如下图,仅需要一次数据 copy,从硬件设备 copy 数据到内核空间。
transferTo、transferFrom
跟进源码,transferFrom 实际使用了 MappedByteBuffer,这里就不赘述了。重点来看下 transferTo,这就是很多框架宣称“零拷贝”的原理。
FileChannel inChannel = FileChannel.open(Paths.get("xxx.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("xxx2.txt"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
inChannel.transferTo(0,inChannel.size(),outChannel);
这里调用了 Linux 操作系统提供的 sendFile 函数,它的交互过程如下图所示:
实际上还是存在在数据copy,数据依然是从硬件复制到内核空间,再由内核空间复制到网卡,对操作系统而言,不存在冗余的复制,这就是“零拷贝”。