Java内存映射,上G大文件轻松处理

内存映射文件(Memory-mapped File),指的是将一段虚拟内存逐字节映射于一个文件,使得应用程序处理文件如同访问主内存(但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作),这要比直接文件读写快几个数量级。

稍微解释一下虚拟内存(很明显,不是物理内存),它是计算机系统内存管理的一种技术。像施了妖法一样使得应用程序认为它拥有连续的可用的内存,实际上呢,它通常是被分隔成多个物理内存的碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

内存映射文件主要的用处是增加 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会导致碎片空间的浪费,因为内存映射总是要对齐页边界,最小单位是 4 KiB,一个 5 KiB 的文件将会映射占用 8 KiB 内存,也就会浪费 3 KiB 内存。

java.nio 包使得内存映射变得非常简单,其中的核心类叫做 MappedByteBuffer,字面意思为映射的字节缓冲区。

01、使用 MappedByteBuffer 读取文件

假设现在有一个文件,名叫 cmower.txt,里面的内容是:

沉默王二,一个有趣的程序员

PS:哎,改不了王婆卖瓜自卖自夸这个臭毛病了,因为文章被盗得都怕了。

这个文件放在 /resource 目录下,我们可以通过下面的方法获取到它:

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,这就意味着我们是从内存中而不是磁盘中读入的文件内容,所以速度会非常快。

02、使用 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 查看一下内容,确认预期的内容有没有写入成功。

03、MappedByteBuffer 的遗憾

据说,在 Java 中使用 MappedByteBuffer 是一件非常麻烦并且痛苦的事,主要表现有:

1)一次 map 的大小最好限制在 1.5G 左右,重复 map 会增加虚拟内存回收和重新分配的压力。也就是说,如果文件大小不确定的话,就不太友好。

2)虚拟内存由操作系统来决定什么时候刷新到磁盘,这个时间不太容易被程序控制。

3)MappedByteBuffer 的回收方式比较诡异。

再次强调,这三种说法都是据说,我暂时能力有限,也不能确定这种说法的准确性,很遗憾。

04、比较文件操作的处理时间

嗨,朋友,阅读完以上的内容之后,我想你一定对内存映射文件有了大致的了解。但我相信,如果你是一名负责任的程序员,你一定还想知道:内存映射文件的读取速度究竟有多快。

为了得出结论,我叫了另外三名竞赛的选手: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大文件轻松处理

05、最后

本篇文章主要介绍了 Java 的内存映射文件,MappedByteBuffer 是其灵魂,读取速度快如火箭。另外,所有这些示例和代码片段都可以在 GitHub(地址如下)上找到——这是一个 Maven 项目,所以它很容易导入和运行。

https://github.com/qinggee/java

精彩回顾:

Java 代码界 3% 的王者?看我是如何解错这 5 道题的

Java:控制反转(IoC)与依赖注入(DI)

Java:前程似锦的 NIO 2.0

< END >

本文分享自微信公众号 - 沉默王二(cmower)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java架构

毕业了5年的同学突然告诉我,他已经是架构师了

作为一名程序员,都会对自己未来的职业发展而焦虑。一方面是因为IT作为知识密集型的行业,知识体系复杂且知识更新速度非常快,“一日不学就会落后”。

10320
来自专栏泰斗贤若如

终极版Servlet——我只能提示您路过别错过

前言:这两天看了SSM框架,本来是想往后继续学的,脑门一转又回来了,不能就这么不声不响的走了,看了这么多天的Servlet,再写最后一篇做个告别吧,这篇起名为终...

8430
来自专栏AI科技大本营的专栏

优雅编写Python3的62个小贴士!

▍42、创建一个迭代器,它从iterable中过滤元素,只返回谓词为False的元素

9940
来自专栏happyJared

Optional 和 Stream

Optional 不是函数式接口,而是用于防止 NullPointerException 的一个工具类。

16930
来自专栏苦逼的码农

在工作中常用到的SQL

最近在公司做了几张报表,还记得刚开始要做报表的时候都快把SQL给忘光了(当时在广州休假了1个月多,在实习期间也没咋写过SQL),回到公司的第一个需求就是做报表。

7110
来自专栏web前端教室

前端开发工具你最喜欢哪一个?

工欲善于其事,必先利其器,下面是一些常用的web前端的开发工具,哪个是你最喜欢 的?或是有哪些下面还没有列出来?在文章结尾留言告诉我吧,

24410
来自专栏java老实人

java学习笔记(中级篇)—单例模式

我们知道JVM的内存资源是很宝贵的,尤其是在程序很庞大的时候,尽量减少创建不必要的对象是能够很大程度的减少内存的开销。单例模式的好处就是能够节省内存,限制了实例...

6610
来自专栏JAVA葵花宝典

全链路跟踪(压测)必备基础组件之线程上下文“三剑客”

说起本地线程变量,我相信大家首先会想到的是JDK默认提供的ThreadLocal,用来存储在整个调用链中都需要访问的数据,并且是线程安全的。由于本文的写作背景是...

12030
来自专栏Modeng的专栏

python 基础认识,弱类型,解释执行

我们从第一个「hello world」开始。首先我们先进入电脑的控制台输入 python3 进入 python 解释器模式。

10320
来自专栏Android补给站

Android 热修复解析

https://github.com/5A59/android-training/tree/master/common-tec/CommonTec

15040

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励