使用 MappedByteBuffer 进行超大文件读写 - 今日头条

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

我们知道,如果使用传统的流读写,这么大的文件,内存直接爆了,根本不可能完成。它使得应用程序认为它拥有连续的可用的内存,而实际上,它通常是被分隔成

最近遇到一个需求,要做超大文件的读写(2G 以上)。我们知道,如果使用传统的流读写,这么大的文件,内存直接爆了,根本不可能完成。由此研究到了 MappedByteBuffer

MappedByteBuffer 的一个能力就是它可以让我们读写那些因为太大而不能放进内存中的文件。有了它,我们就可以假定整个文件都放在内存中(实际上,大文件放在内存和虚拟内存中),基本上都可以将它当作一个特别大的数组来访问,这样极大的简化了对于大文件的修改等操作。

MappedByteBuffer 底层使用的技术是内存映射。

所以讲 MappedByteBuffer 之前,先讲下计算机的内存管理

  • MMU:CPU 的内存管理单元。
  • 物理内存:即内存条的内存空间。
  • 虚拟内存:计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。虚拟内存一般使用的是页面映像文件,即硬盘中的某个 (某些) 特殊的文件. 操作系统负责页面文件内容的读写,这个过程叫 “页面中断 / 切换”。
  • 页文件:操作系统反映构建并使用虚拟内存的硬盘空间大小而创建的文件,在 windows 下,即 pagefile.sys 文件,其存在意味着物理内存被占满后,将暂时不用的数据移动到硬盘上。
  • 缺页中断:当程序试图访问已映射在虚拟地址空间中但未被加载至物理内存的一个分页时,由 MMC 发出的中断。如果操作系统判断此次访问是有效的,则尝试将相关的页从虚拟内存文件中载入物理内存。

如果正在运行的一个进程,它所需的内存是有可能大于内存条容量之和的,如内存条是 256M,程序却要创建一个 2G 的数据区,那么所有数据不可能都加载到内存(物理内存),必然有数据要放到其他介质中(比如硬盘),待进程需要访问那部分数据时,再调度进入物理内存。

假设你的计算机是 32 位,那么它的地址总线是 32 位的,也就是它可以寻址 00xFFFFFFFF(4G)的地址空间,但如果你的计算机只有 256M 的物理内存 0x0x0FFFFFFF(256M),同时你的进程产生了一个不在这 256M 地址空间中的地址,那么计算机该如何处理呢?回答这个问题前,先说明计算机的内存分页机制。

计算机会对虚拟内存地址空间(32 位为 4G)进行分页从而产生页(page),对物理内存地址空间(假设 256M)进行分页产生页帧(page frame),页和页帧的大小一样,所以虚拟内存页的个数势必要大于物理内存页帧的个数。在计算机上有一个页表(page table),就是映射虚拟内存页到物理内存页的,更确切的说是页号到页帧号的映射,而且是一对一的映射。

问题来了,虚拟内存页的个数 > 物理内存页帧的个数,岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间?不是的,操作系统是这样处理的。操作系统有个页面失效(page fault)功能。操作系统找到一个最少使用的页帧,使之失效,并把它写入磁盘,随后把需要访问的页放到页帧中,并修改页表中的映射,保证了所有的页都会被调度。

虚拟内存地址:由页号(与页表中的页号关联)和偏移量(页的小大,即这个页能存多少数据)组成。

举个例子,有一个虚拟地址它的页号是 4,偏移量是 20,那么他的寻址过程是这样的:首先到页表中找到页号 4 对应的页帧号(比如为 8),如果页不在内存中,则用失效机制调入页,接着把页帧号和偏移量传给 MMU 组成一个物理上真正存在的地址,最后就是访问物理内存的数据了。

对大多数操作系统来说,做内存文件映射都是一个昂贵的操作。所以 MappedByteBuffer 适用于对大文件的读写。对于小文件直接用普通的读写就好了。

MappedByteBuffer 继承自 ByteBuffer,所以 ByteBuffer 有的能力它全有;像变动 position 和 limit 指针啦、包装一个其他种类 Buffer 的视图啦,都可以。

你可以把整个文件 (不管文件有多大) 看成是一个 ByteBuffer。

1
2
3
4
java.lang.Object
 java.nio.Buffer
 java.nio.ByteBuffer
 java.nio.MappedByteBuffer

一个简单的读写示例

https://p9.toutiaoimg.com/origin/pgc-image/486ef4807fa4437eb42e460561eba894?from=pc

使用 MappedByteBuffer 整个过程非常快。

映射的字节缓冲区是通过 FileChannel.map 方法创建的。映射的字节缓冲区和它所表示的文件映射关系在该缓冲区本身成为垃圾回收缓冲区之前一直保持有效。

官方是这样说的:

The buffer and the mapping that it represents will remain valid until the buffer itself is garbage-collected.

A mapping, once established, is not dependent upon the file channel that was used to create it. Closing the channel, in particular, has no effect upon the validity of the mapping.

这就可能一些问题,主要就是内存占用和文件关闭等不确定问题。被 MappedByteBuffer 打开的文件只有在垃圾收集时才会被关闭,而这个点是不确定的。

比如说,先用 MappedByteBuffer map 到一个源文件。进行复制操作。结束后想删掉源文件。

删除是会失败的,主要原因是变量 MappedByteBuffer 仍然持有源文件的句柄,文件处于不可删除状态。

但是和官方并没有给出释放句柄的操作。网上有人贡献一段代码,可以解决这个问题。

https://p9.toutiaoimg.com/origin/pgc-image/8862d2b8f832485199956759a29b5be6?from=pc

估计后续官方会优化掉这个缺陷。

参考文章

https://www.jianshu.com/p/f90866dcbffc

https://www.cnblogs.com/ironPhoenix/p/4204472.html