MappedByteBuffer 是Java NIO中引入的一种硬盘物理文件和内存映射方式,当物理文件较大时,采用MappedByteBuffer,读写性能较高,其内部的核心实现是DirectByteBuffer(JVM 堆外直接物理内存)。
JVM 进程通过内存映射方式加载的物理文件并不会耗费同等大小的物理内存。当应用程序访问数据时,程序通过虚拟地址寻址对应的内存页,如果物理内存中不存在对应页,MMU则会产生缺页中断异常,CPU尝试从系统Swap分区中查找,如仍不存在,则会直接从硬盘中物理文件中读取。
传统的基于文件流的方式读取文件方式是系统指令调用,文件数据首先会被读取到进程的内核空间的缓冲区,而后复制到进程的用户空间,这个过程中存在两次数据拷贝;而内存映射方式读取文件的方式,也是系统指令调用,在产生缺页中断后,CPU直接从磁盘文件load数据到进程的用户空间,只有一次数据拷贝。
FileChannel提供了map方法把磁盘文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。
内存映像文件访问的方式,共三种:
a) MapMode.READ_ONLY:只读,试图修改得到的缓冲区将导致抛出异常。 b) MapMode.READ_WRITE:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的。 c) MapMode.PRIVATE:私用,可读可写,但是修改的内容不会写入文件,只是buffer自身的改变。
MappedByteBuffer在处理大文件时的确性能很高,但也存在一些问题,其所对应的内存使用的是JVM堆外内存,JVM young gc和CMS gc并不能触发回收MappedByteBuffer对应的内存,只有full gc(stop the world的方式)可以使其回收内存,堆外直接内存会根据自己的情况(当需要新分配直接内存时,如果所剩堆外内存空间不够,第一次产生OutOfMemoryError时)来触发 System.gc(),此处有坑,若JVM配置了参数-XX:DisableExplicitGC,System.gc()将不会触发full gc,最终导致内存泄漏。而且触发其内存回收的时间点是不确定的。Java api文档中标注:
在应用程序频繁使用堆外内存时,还可以通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉游离状态的堆外内存。
因此,在使用堆外内存高性能的福利的同时,及时的回收掉废弃掉的内存是十分关键的。
性能分析
从代码层面上看,从硬盘上将文件读入内存,都要经过文件系统进行数据拷贝,并且数据拷贝操作是由文件系统和硬件驱动实现的,理论上来说,拷贝数据的效率是一样的。
但是通过内存映射的方法访问硬盘上的文件,效率要比read和write系统调用高,这是为什么?
read()是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝;
map()也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝。
所以,采用内存映射的读写效率要比传统的read/write性能高。
拷贝视频代码举例:
机器配置: 内存8G CPU 4核(i5-3210M)
第一种方式:
long start = System.currentTimeMillis();
FileInputStream fis =new FileInputStream("d:\\追龙2.mp4");
FileChannel in = fis.getChannel();
FileOutputStream fos =new FileOutputStream("e:\\t.mp4");
FileChannel out = fos.getChannel();
out.transferFrom(in,0,in.size());
fis.close();
fos.close();
in.close();
out.close();
log.info(" 消耗时间:{} 秒",(System.currentTimeMillis()-start)/1000);
1.28G 大约消耗28秒时间
第二种方式:
long start = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("d:/追龙2.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("e:/追龙2.mp4"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//内存映射文件
MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY,0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE,0, inChannel.size());
byte[] dst =new byte[1024];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
inMappedBuf.force();
outMappedBuf.force();
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
log.info("拷贝文件消耗时间{}",(end-start)/1000);
同样1.28G ,消耗时时间不到1秒
但是 ,第二种方式,拷贝的视频文件,不能播放,不知道什么因素,如果有知道解决方案的,麻烦给我留言一下或者email 80692072@qq.com 谢谢。