内存映射文件(Memory-mapped File),指的是将一段虚拟内存逐字节映射于一个文件,使得应用程序处理文件如同访问主内存(但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操做),这要比直接文件读写快几个数量级。java
稍微解释一下虚拟内存(很明显,不是物理内存),它是计算机系统内存管理的一种技术。像施了妖法同样使得应用程序认为它拥有连续的可用的内存,实际上呢,它一般是被分隔成多个物理内存的碎片,还有部分暂时存储在外部磁盘存储器上,在须要时进行数据交换。git
内存映射文件主要的用处是增长 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会致使碎片空间的浪费,由于内存映射老是要对齐页边界,最小单位是 4 KiB,一个 5 KiB 的文件将会映射占用 8 KiB 内存,也就会浪费 3 KiB 内存。程序员
java.nio 包使得内存映射变得很是简单,其中的核心类叫作 MappedByteBuffer,字面意思为映射的字节缓冲区。github
假设如今有一个文件,名叫 cmower.txt,里面的内容是:算法
沉默王二,一个有趣的程序员bash
PS:哎,改不了王婆卖瓜自卖自诩这个臭毛病了,由于文章被盗得都怕了。app
这个文件放在 /resource
目录下,咱们能够经过下面的方法获取到它:dom
ClassLoader classLoader = Cmower.class.getClassLoader();
Path path = Paths.get(classLoader.getResource("cmower.txt").getPath());
复制代码
Path 既能够表示一个目录,也能够表示一个文件,就像 File 那样——固然了,Path 是用来取代 File 的。性能
而后,从文件中获取一个 channel(通道,对磁盘文件的一种抽象)。测试
FileChannel fileChannel = FileChannel.open(path);
复制代码
紧接着,调用 FileChannel 类的 map 方法从 channel 中获取 MappedByteBuffer,此类扩展了 ByteBuffer
——提供了一些内存映射文件的基本操做方法。
MappedByteBuffer mappedByteBuffer = fileChannel.map(mode, position, size);
复制代码
稍微解释一下 map 方法的三个参数。
1)mode 为文件映射模式,分为三种:
MapMode.READ_ONLY(只读),任何试图修改缓冲区的操做将致使抛出 ReadOnlyBufferException 异常。
MapMode.READ_WRITE(读/写),任何对缓冲区的更改都会在某个时刻写入文件中。须要注意的是,其余映射同一个文件的程序可能不能当即看到这些修改,多个程序同时进行文件映射的行为依赖于操做系统。
MapMode.PRIVATE(私有), 对缓冲区的更改不会被写入到该文件,任何修改对这个缓冲区来讲都是私有的。
2)position 为文件映射时的起始位置。
3)size
为要映射的区域的大小,必须是非负数,不得大于Integer.MAX_VALUE
。
一旦把文件映射到内存缓冲区,咱们就能够把里面的数据读入到 CharBuffer 中并打印出来。具体的代码示例以下。
CharBuffer charBuffer = null;
ClassLoader classLoader = Cmower.class.getClassLoader();
Path path = Paths.get(classLoader.getResource("cmower.txt").getPath());
try (FileChannel fileChannel = FileChannel.open(path)) {
MappedByteBuffer mappedByteBuffer = fileChannel.map(MapMode.READ_ONLY, 0, fileChannel.size());
if (mappedByteBuffer != null) {
charBuffer = Charset.forName("UTF-8").decode(mappedByteBuffer);
}
System.out.println(charBuffer.toString());
} catch (IOException e) {
e.printStackTrace();
}
复制代码
因为 decode()
方法的参数是 MappedByteBuffer,这就意味着咱们是从内存中而不是磁盘中读入的文件内容,因此速度会很是快。
假设如今要把下面的内容写入到一个文件,名叫 cmower1.txt。
沉默王二,《Web全栈开发进阶之路》做者
这个文件尚未建立,计划放在项目的 classpath 目录下。
Path path = Paths.get("cmower1.txt");
复制代码
具体位置见下图所示。
而后,建立文件的通道。
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)
复制代码
仍然使用的 open 方法,不过增长了 3 个参数,前 2 个很好理解,表示文件可读(READ)、可写(WRITE);第 3 个参数 TRUNCATE_EXISTING 的意思是若是文件已经存在,而且文件已经打开将要进行 WRITE 操做,则其长度被截断为 0。
紧接着,仍然调用 FileChannel 类的 map 方法从 channel 中获取 MappedByteBuffer。
MappedByteBuffer mappedByteBuffer = fileChannel.map(MapMode.READ_WRITE, 0, 1024);
复制代码
这一次,咱们把模式调整为 MapMode.READ_WRITE,而且指定文件大小为 1024,即 1KB 的大小。而后使用 MappedByteBuffer 中的 put() 方法将 CharBuffer 的内容保存到文件中。具体的代码示例以下。
CharBuffer charBuffer = CharBuffer.wrap("沉默王二,《Web全栈开发进阶之路》做者");
Path path = Paths.get("cmower1.txt");
try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
MappedByteBuffer mappedByteBuffer = fileChannel.map(MapMode.READ_WRITE, 0, 1024);
if (mappedByteBuffer != null) {
mappedByteBuffer.put(Charset.forName("UTF-8").encode(charBuffer));
}
} catch (IOException e) {
e.printStackTrace();
}
复制代码
能够打开 cmower1.txt 查看一下内容,确认预期的内容有没有写入成功。
听说,在 Java 中使用 MappedByteBuffer 是一件很是麻烦而且痛苦的事,主要表现有:
1)一次 map 的大小最好限制在 1.5G 左右,重复 map 会增长虚拟内存回收和从新分配的压力。也就是说,若是文件大小不肯定的话,就不太友好。
2)虚拟内存由操做系统来决定何时刷新到磁盘,这个时间不太容易被程序控制。
3)MappedByteBuffer 的回收方式比较诡异。
再次强调,这三种说法都是听说,我暂时能力有限,也不能肯定这种说法的准确性,很遗憾。
嗨,朋友,阅读完以上的内容以后,我想你必定对内存映射文件有了大体的了解。但我相信,若是你是一名负责任的程序员,你必定还想知道:内存映射文件的读取速度究竟有多快。
为了得出结论,我叫了另外三名竞赛的选手:InputStream(普通输入流)、BufferedInputStream(带缓冲的输入流)、RandomAccessFile(随机访问文件)。
读取的对象是加勒比海盗4惊涛怪浪.mkv,大小为 1.71G。
1)普通输入流
public static void inputStream(Path filename) {
try (InputStream is = Files.newInputStream(filename)) {
int c;
while((c = is.read()) != -1) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
复制代码
2)带缓冲的输入流
public static void bufferedInputStream(Path filename) {
try (InputStream is = new BufferedInputStream(Files.newInputStream(filename))) {
int c;
while((c = is.read()) != -1) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
复制代码
3)随机访问文件
public static void randomAccessFile(Path filename) {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(filename.toFile(), "r")) {
for (long i = 0; i < randomAccessFile.length(); i++) {
randomAccessFile.seek(i);
}
} catch (IOException e) {
e.printStackTrace();
}
}
复制代码
4)内存映射文件
public static void mappedFile(Path filename) {
try (FileChannel fileChannel = FileChannel.open(filename)) {
long size = fileChannel.size();
MappedByteBuffer mappedByteBuffer = fileChannel.map(MapMode.READ_ONLY, 0, size);
for (int i = 0; i < size; i++) {
mappedByteBuffer.get(i);
}
} catch (IOException e) {
e.printStackTrace();
}
}
复制代码
测试程序也很简单,大体以下:
long start = System.currentTimeMillis();
bufferedInputStream(Paths.get("jialebi.mkv"));
long end = System.currentTimeMillis();
System.out.println(end-start);
复制代码
四名选手的结果以下表所示。
方法 | 时间 |
---|---|
普通输入流 | 龟速,没有耐心等出结果 |
随机访问文件 | 龟速,没有耐心等下去 |
带缓冲的输入流 | 29966 |
内存映射文件 | 914 |
普通输入流和随机访问文件都慢得要命,真的是龟速,我没有耐心等待出结果;带缓冲的输入流的表现还不错,但相比内存映射文件就逊色多了。由此得出的结论就是:内存映射文件,上G大文件轻松处理。
本篇文章主要介绍了 Java 的内存映射文件,MappedByteBuffer 是其灵魂,读取速度快如火箭。另外,全部这些示例和代码片断均可以在 GitHub 上找到——这是一个 Maven 项目,因此它很容易导入和运行。
欢迎关注「沉默王二」公众号,后台回复关键字「Java」获取 Java 经常使用算法手册——成为高手的必备手册。