一、什么是内存映射文件
内存映射文件,是由一个文件到一块内存的映射,可以理解为将一个文件映射到进程地址,然后可以通过操作内存来访问文件数据。说白了就是使用虚拟内存将磁盘的文件数据加载到虚拟内存的内存页,然后就可以直接操作内存页数据。
我们读写一个文件使用read()和write()方法,这两个方法是调用系统底层接口来传输数据,因为内核空间的文件页和用户空间的缓冲区没有一一对应,所以读写数据时会在内核空间和用户空间之间进行数据拷贝,在操作大量文件数据时会导致性能很低,使用内存映射文件可以非常高效的操作大量文件数据。
通过内存映射机制操作文件比使用常规方法和使用FileChannel读写高效的多。
内存映射文件使用文件系统建立从用户空间到可用文件系统页的虚拟内存映射,这样做有以下好处:
- 用户进程把文件数据当内存数据,无需调用read()或write()
- 当用户进程接触到映射内存空间,会自动产生页错误,从而将文件数据从磁盘读到内存;若用户空间进程修改了内存页数据,相关页会自动标记并刷新到磁盘,文件被更新
- 操作系统的虚拟内存对内存页进行高速缓存,自动根据系统负载进行内存管理
- 用户空间和内核空间的数据总是一一对应,无需执行缓冲区拷贝
- 大数据的文件使用映射,无需消耗大量内存即可进行数据拷贝
二、如何创建内存映射文件
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
FileChannel fc = raf.getChannel();
//将test.txt文件所有数据映射到虚拟内存,并只读
MappedByteBuffer mbuff = fc.map(MapMode.READ_ONLY, 0, fc.size());
映射文件的范围不应超过文件的实际大小,否则文件的大小会被增大到指定的大小
//实际文件只有10个字节,执行下面代码后,文件内容变为10000个字节
MappedByteBuffer mbuff = fc.map(MapMode.READ_WRITE, 0, 10000);
第一个参数MapMode是个三个值:
- MapMode.READ_ONLY:只读,若FileChannel不可读,抛出NonReadableChannelException
- MapMode.READ_WRITE:可读写,若FileChannel不可写,抛出NonWritableChannelException
- MapMode.PRIVATE:创建一个写时拷贝的映射,修改映射内存页的数据只对MappedByteBuffer可视
三、MappedByteBuffer API
MappedByteBuffer是ByteBuffer的子类,所以可被通道读写。MappedByteBuffer提供的方法:
- load():加载整个文件到内存
- isLoaded():判断文件数据是否全部加载到了内存
- force():将缓冲区的更改刷新到磁盘
四、通道到通道传输
将文件数据从一个通道传输到另一个通道,FileChannel提供下面2个高效方法:
- transferTo(long position, long count, WritableByteChannel target):传输到哪个可写通道
- transferFrom(ReadableByteChannel src, long position, long count):从哪个可读通道传输过来
通道到通道传输数据使用上面2个方法不需要使用中间缓冲区。只有FileChannel有以上方法,所以Channel-to-Channel中必须有一个是FileChannel。对于传输大量数据,以上2个方法的效率是非常高的。
下面是个例子
//获取test0.txt文件的通道句柄
RandomAccessFile raf0 = new RandomAccessFile("test0.txt", "r");
FileChannel fc0 = raf0.getChannel();
//获取test1.txt文件的通道句柄
RandomAccessFile raf1 = new RandomAccessFile("test1.txt", "rw");
FileChannel fc1 = raf1.getChannel();
//将test0.txt传输到test1.txt
fc0.transferTo(0, fc0.size(), fc1);
//强制刷新数据到磁盘
fc0.force(true);
//关闭通道
raf1.close();
raf0.close();