java io操作中通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高。
MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)方法的作 用是将此通道的文件区域直接映射到内存中 。 可以通过下列 3 种模式将文件区域映射到内存中 。
1 ) 只读:试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException异常。 (MapMode. READ ONL Y)
2 ) 读取/写人:对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件 的其他程序不一定是可见的 。 (MapMode.READ_WRITE)
3 ) 专用:对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其 他程序也不是可见的;相反,会创建缓冲区已修改部分的专用副本。 (MapMode.PRIVATE)
对于只读映射关系,此通道必须可以进行读取操作;对于 读 取 /写入或专用映射关系, 此通道必须可以进行读取和写人操作 。
此方法返回的已映射字节缓冲区位置为零,限制和容量为 size;其标记是不确定的。 在 缓冲区本身被作为垃圾回收之前,该缓 冲区及其表示的映射关系都是有效的 。
映射关系一经创建,就不再依赖于创建它时所用的文件通道 。 特别是关闭该通道对映 射关系的有效性没有任何影响 。
很多内存映射文件的细节从根本上是取决于底层操作系统的,因此是未指定的 。 当所 请求的区域没有完全包含在此通道的文件中时,此方法的行为是未指定的: 未指定是否将此 程序或另一个程序对底层文件的内容或大小所进行的更改传播到缓冲区;未指定将对缓冲区
的更改传播到文件的频 率 。
对于大多数操作系统而 言 ,与通过普通的 read()和l write()方法读取或写入数千字 节的
数据相比,将文件映射到内存中开销更大。 从性能的观点来看,通常将相对较大的文件映射 到内存中才是值得的 。
该方法的 3个参数的说明如下。
1) mode:根据只读、读取/写入或专用(写入时复制)来映射文件,分别为 FileChannel. MapMode 类中所定义的 READ_ONLY、 READ_WRITE 和 PRIVATE;
2) position: 文件中的位置,映射区域从此位置开始; 必须为非负数 。
3 ) size : 要映射的区域大小;必须为非负数且不大于 Integer.MAX VALUE。
MappedByteBuffer类,它是直接字节缓冲区,其内容是文件的内存映射 区域。 映射的字节缓冲区是通过 FileChannel.map()方法创建的。 此类用特定于内存映射文 件区域的操作扩展 ByteBuffer类 。
映射的字节 缓 冲 区 和它所表 示 的文件映射关系 在该缓 冲区本身成为垃圾回收 缓 冲区之 前一直保持有效 。
映射的字节缓冲区的内容可以随时更改,如在此程序或另一个程序更改了对应的映射 文件区域的内容的情况下。 这些更改是否发生(以及何时发生)与操作系统无关,因此是未 指定的 。
方法如图:
示例代码:
public class MappedByteBufferTest {
public static void main(String[] args) {
File file = new File("D://data.txt");
long len = file.length();
byte[] ds = new byte[(int) len];
try {
MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r")
.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, len);
for (int offset = 0; offset < len; offset++) {
byte b = mappedByteBuffer.get();
ds[offset] = b;
}
Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
while (scan.hasNext()) {
System.out.print(scan.next() + " ");
}
} catch (IOException e) {}
}
}
性能分析
从代码层面上看,从硬盘上将文件读入内存,都要经过文件系统进行数据拷贝,并且数据拷贝操作是由文件系统和硬件驱动实现的,理论上来说,拷贝数据的效率是一样的。
但是通过内存映射的方法访问硬盘上的文件,效率要比read和write系统调用高,这是为什么?
- read()是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝;
- map()也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝。
所以,采用内存映射的读写效率要比传统的read/write性能高。
总结
- MappedByteBuffer使用虚拟内存,因此分配(map)的内存大小不受JVM的-Xmx参数限制,但是也是有大小限制的。
- 如果当文件超出1.5G限制时,可以通过position参数重新map文件后面的内容。
- MappedByteBuffer在处理大文件时的确性能很高,但也存在一些问题,如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。
javadoc中也提到:A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected.*