简介
通常情况下,当我们使用Java的BIO读磁盘的时候(read/write),需要对操作系统进行系统调用,OS先将磁盘上的数据读取到内核空间,然后从内核空间复制数据到用户空间,最后用户程序对用户空间中的数据进行操作。同理,在进行写磁盘的时候,OS会先将用户空间的数据复制到内核空间,再从内核空间写入到磁盘。由于数据会在用户空间和内核空间中复制,在数据量很大的时候会影响IO操作的性能。因此Java在1.4中引入了NIO,其能过通过Channel对磁盘进行IO操作,在复制文件、映射文件等场景下能够避免数据在用户空间和内核空间之间进行复制,能够依靠操作系统提供的机制(Linux下为sendfile64函数)直接访问内核空间的数据,从而提高效率。
mmap在处理大文件的时候有一定的优势,一方面不用把全部数据都加载到内存,可以通过MappedByteBuffer的position来设置获取数据的位置,另一方面可以使用虚拟内存来映射超过物理内存大小的大文件。
mmap也是有一定的缺点,并不是说mmap就一定比BIO性能好,需要具体问题具体分析。mmap的缺点有几个方面,一是为了创建并维持地址空间与文件的映射关系,内核中需要有特定的数据结构来实现这一映射,这当然是有性能开销的,除此之外另一点就是缺页问题,page fault。缺页中断也是有开销的,而且不同的内核由于内部的实现机制不同,其系统调用、数据copy以及缺页处理的开销也不同,因此就性能上来说我们不能肯定的说mmap就比标准IO好。这要看标准IO中的系统调用、内存调用的开销与mmap方法中的缺页中断处理的开销哪个更小,开销小的一方将展现出更优异的性能。
注意,mmap与虚拟内存的结合在处理大文件时可以简化代码设计,但在性能上是否优于传统的read/write方法就不一定了,还是那句话关于mmap与传统IO在涉及到性能时你需要基于真实的应用场景测试。使用mmap处理大文件要注意,如果系统是32位的话,进程的地址空间就只有4G,这其中还有一部分预留给操作系统,因此在32位系统下可能不足以在你的进程地址空间中找到一块连续的空间来映射该文件,在64位系统下则无需担心地址空间不足的问题,这一点要注意。
Kafka在Consumer端对稀疏索引的操作使用了mmap,其将稀疏索引文件进行内存映射,这不会招致系统调用以及额外的内存复制开销,从而提高了文件读取效率。
传统拷贝文件流程
使用mmap进行内存映射
Java实例
下面的Java实例演示了如何通过Channel读写文件,以及如何copy文件和使用mmap读写文件。
可以看出,Channel拷贝文件代码非常简单,仅需调用transferFrom或transferTo即可。通过MappedBufferByte操作缓存就会直接体现到文件,非常方便。
package com.my.kafka.io;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
public class TestChannel {
private static String FILE_PATH = "C:\\temp\\test_channel.txt";
public static void main(String[] args) {
testWriteString();
testReadString();
testCopyFile();
testMMap();
}
/*
* Write data via channel
*/
private static void testWriteString() {
try (RandomAccessFile raf = new RandomAccessFile(FILE_PATH, "rw"); FileChannel channel = raf.getChannel()) {
String str = "This is my string.";
ByteBuffer buffer = ByteBuffer.allocate(str.getBytes().length);
buffer.clear();
buffer.put(str.getBytes());
buffer.flip(); // Change mode to read, invoke this method when reading data from buffer
channel.write(buffer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
}
/*
* Read 1KB data via channel.
* 0....limit....capacity
* p
* p is current position.
*/
private static void testReadString() {
try (RandomAccessFile raf = new RandomAccessFile(FILE_PATH, "rw"); FileChannel channel = raf.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
channel.read(buffer);
buffer.flip(); // Change mode to read, invoke this method when reading data from buffer
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println(new String(bytes));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/*
* Use Channel transerTo or transerFrom to copy file, now matter how big the
* file is. Will invoke native method "transferTo0". On Linux invokes method:
* FileChannelImpl.c#Java_sun_nio_ch_FileChannelImpl_transferTo0 -> sendfile64
*/
private static void testCopyFile() {
try (RandomAccessFile rafOri = new RandomAccessFile(FILE_PATH, "r");
RandomAccessFile rafDest = new RandomAccessFile(FILE_PATH + ".dest", "rw");
FileChannel oriChannel = rafOri.getChannel();
FileChannel destChannel = rafDest.getChannel();) {
// destChannel.transferFrom(oriChannel, 0, oriChannel.size());
oriChannel.transferTo(0, oriChannel.size(), destChannel);
} catch (Exception e) {
e.printStackTrace();
}
}
/*
* Map file into a memory space(MappedByteBuffer), when put data to
* MappedByteBuffer, data will be flushed to disk automatically. It supports to
* change "position" to get data from specified position.
*/
private static void testMMap() {
try (RandomAccessFile raf = new RandomAccessFile(FILE_PATH + ".mmap.txt", "rw");
FileChannel channel = raf.getChannel()) {
// Map file with 1024 bytes, will create new file if does not exist
// When put data, will override existing data one by one
// When put data to MappedByteBuffer, data will be flushed to disk, no need
// use channel to flush data
MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_WRITE, 0, 1024);
mappedBuffer.put("This is my string.\n".getBytes()); // Only put data to buffer
mappedBuffer.put("Hello MappedByteBuffer.\n".getBytes());
mappedBuffer.flip();
byte[] bytes = new byte[18]; // Only get 18 bytes data from position 0
mappedBuffer.get(bytes);
System.out.println(new String(bytes));
// Change position to 11, then get next 18 bytes from position 11
mappedBuffer.position(11);
bytes = new byte[18];
mappedBuffer.get(bytes);
System.out.println(new String(bytes));
} catch (Exception e) {
e.printStackTrace();
}
}
}
部分内容参考自:mmap可以让程序员解锁哪些骚操作?