深入理解零拷贝
在操作系统的层面上分为两个空间:用户空间与内核空间
内核空间和用户空间一般通过系统调用进行通信。
用户空间就是用户进程所在的内存区域,系统空间就是操作系统占据的内存区域。 用户进程和系统进程的所有数据都在内存中。
出现了四次上下文切换和两次不必要的数据拷贝。
两次不必要的数据拷贝分别为:从内核空间将数据拷贝至用户空间,将用户空间的数据再次拷贝至内核空间。
用户空间只是一个中转的媒介,对数据没有做任何的处理。
出现了四次数据的拷贝
传统同步I/O存在的问题:上下文切换次数过多,存在没有意义的拷贝。(在这方面入手提高性能)
出现了两次上下文切换。
相对于传统I/O减少了两次数据拷贝的过程。从内核空间拷贝数据至用户空间。从用户空间拷贝数据值内核空间。
从操作系统层面来看这就是零拷贝,因为不再有数据会在用户空间和内核空间之间拷贝。所有的操作都在内核空间内进行。
出现了两次数据拷贝
进一步分析 -> 可不可以从磁盘直接将数据读取到socket缓冲区? 从而再减少一次数据的拷贝。
出现一次数据拷贝
我们发现Netty中的零拷贝 比 操作系统意义上的零拷贝还少了一次数据的拷贝。
我们并没有将数据再拷贝至socket buffer中,而是在其中存储的文件描述符,
再然后协议引擎直接从内核缓冲区和socket buffer中收集数据,直接发送至服务器端。
但是注意->需要操作系统支持Gather。
就出现了一次数据拷贝
使用案例进行测试
案例一、使用传统I/O方式
//服务端
Server{
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8899);
while (true){
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
try {
byte[] byteArray = new byte[1024];
while (true){
int read = dataInputStream.read(byteArray, 0, byteArray.length);
if (read == -1){
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
//客户端
Client{
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",8899);
String fileName = "D:/Users/mjw/Downloads/eclipse.zip";
InputStream inputStream = new FileInputStream(fileName);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[1024];
long readCount;
long total = 0;
long startTime = System.currentTimeMillis();
while ((readCount = inputStream.read(buffer)) >=0){
total += readCount;
dataOutputStream.write(buffer);
}
System.out.println("发送总字节数:" + total +", 耗时" + (System.currentTimeMillis() - startTime));
dataOutputStream.close();
socket.close();
inputStream.close();
}
}
-----------------------测试结果--------------------
耗时:15453毫秒
案例二、使用NIO方式
//服务端
Server{
public static void main(String[] args) throws Exception{
InetSocketAddress address = new InetSocketAddress(8898);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket socket = serverSocketChannel.socket();
socket.setReuseAddress(true);
socket.bind(address);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(true);
int readCount = 0;
while (-1 != readCount){
try {
readCount = socketChannel.read(byteBuffer);
}catch (Exception e){
e.printStackTrace();
}
byteBuffer.rewind();
}
}
}
}
//客户端
Client{
public static void main(String[] args) throws Exception{
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",8898));
socketChannel.configureBlocking(true);
String fileName = "D:/Users/mjw/Downloads/eclipse.zip";
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
long startTime = System.currentTimeMillis();
long transferCount = fileChannel.transferTo(0,fileChannel.size(),socketChannel); //将文件写入到socketChannel
System.out.println("发送字节数:" + transferCount +",耗时" + (System.currentTimeMillis() - startTime));
fileChannel.close();
}
}
---------------------测试结果---------------------
35毫秒
我们发现测试结果相差还是很明显的,拷贝的减少提高了不止一点点的性能。
我们的NIO案例中、最至关重要的一步是 ->
fileChannel.transferTo(0,fileChannel.size(),socketChannel);
它的头部注释中有这么一段话
/**
* This method is potentially much more efficient than a simple loop
* that reads from this channel and writes to the target channel. Many
* operating systems can transfer bytes directly from the filesystem cache
* to the target channel without actually copying them.
*/
此方法可能比从此通道读取数据并写入目标通道的简单循环高效得多。许多操作系统可以直接将字节从文件系统缓存传输到目标通道,而不需要实际复制它们。
原来如此,到这里我已经知道零拷贝的执行过程了,但是又有新的问题出现了。
既然我们不会再有数据拷贝至我们的用户空间,那当我们需要对数据操作的时候怎么办呢?
我们知道传统的I/O操作,我们会在用户空间对拷贝的数据进行操作。
而NIO中我们是使用直接内存映射来实现这一操作的。什么是直接内存?
在《深入理解Java虚拟机》中对于直接内存给出的解释是这样的。直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
而在NIO中,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。因此避免了在Java堆和Native堆中来回复制数据。