简介
内存映射文件是操作系统利用内存,来实现将一个文件或者文件的一部分“映射”到内存中的文件。内存映射文件可当做数组访问,速度比传统文件访问快。
内存映射文件有何意义?
下图是一组测试数据,测试内容是对JDK的jre/lib中37MB rt.jar计算校验和CRC32所需时间。
可以明显看到,内存映射文件比随机访问RandomAccessFile要快很多,不过对比带缓冲的输入流优势不是很明显。
如何进行映射?
- 从文件中获得一个通道(channel),通道是用于磁盘文件的一种抽象,使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
FileChannel channel = FileChannel.open(path, options);
// option 也可以缺省, 因为对缓冲区的读写由映射到缓冲区时传入FileChannel.MapMode参数决定
FileChannel channel = FileChannel.open(Paths.get("rss", "test.txt", StandardOpenOption.READ));
- 通过FileChannel类的map方法,从这个通道获取一个MappedByteBuffer,可以想要映射的文件区域(起始位置、元素个数)和映射模式。
支持的映射模式
映射模式 | 描述 | 备注 |
---|---|---|
FileChannel.MapMode.READ_ONLY | 所产生的缓冲区只读,任何写缓冲区操作将导致ReadOnlyBufferException | |
FileChannel.MapMode.READ_WRITE | 所产生的缓冲区可读写,任何修改都会在某个时刻写回文件中 | 多个程序同时进行文件映射的确切行为依赖于操作系统 |
FileChannel.MapMode.PRIVATE | 多产生的缓冲器可读写,不过修改是私有的,不会影响到文件 |
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, (int)channel.size());
- 通过MappedByteBuffer类的buffer对象,读写数据
注意:MappedByteBuffer缓冲区支持顺序访问和随机访问。
访问内存映射文件缓冲区
通过MappedByteBuffer读写缓冲区
顺序访问示例
while(buffer.hasRemaining()) {
byte b = buffer.get();
...
}
随机访问示例
for(int i = 0; i < buffer.limit(); i ++) {
byte b = buffer.get(i);
...
}
示例
示例代码
比较InputStream(普通输入流), BufferedInputStream(带缓冲的输入流), RandomAccessFile(随机访问文件),MappedByteBuffer(内存映射文件)这几种方式进行文件校验和CRC32计算时间。
// InputStream 计算校验和
public static long checksumInputStream(Path fileName) throws IOException{
try (InputStream in = Files.newInputStream(fileName)) {
CRC32 crc = new CRC32();
int c;
while ((c = in.read()) != -1) {
crc.update(c);
}
return crc.getValue();
}
}
// BufferedInputStream 计算校验和
public static long checksumBufferedInputStream(Path filename) throws IOException {
try(InputStream in = new BufferedInputStream(Files.newInputStream(filename))) {
CRC32 crc = new CRC32();
int c;
while ((c = in.read()) != -1) {
crc.update(c);
}
return crc.getValue();
}
}
// RandomAccessFile 计算校验和
public static long checksumRandomAccessFile(Path filename) throws IOException {
try (RandomAccessFile file = new RandomAccessFile(filename.toFile(), "r")) {
long length = file.length();
CRC32 crc = new CRC32();
for (int i = 0; i < length; i++) {
file.seek(i);
int c = file.readByte();
crc.update(c);
}
return crc.getValue();
}
}
// MappedByteBuffer 计算校验和
public static long checksumMappedFile(Path filename) throws IOException {
try(FileChannel channel = FileChannel.open(filename)) {
CRC32 crc = new CRC32();
int length = (int)channel.size();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length);
for (int i = 0; i < length; i++) {
int c = buffer.get(i);
crc.update(c);
}
return crc.getValue();
}
}
public static void main(String[] args) throws IOException {
System.out.println("Input Stream:");
long start = System.currentTimeMillis();
Path filename = Paths.get("rss", "rt.jar");
long crcValue = checksumInputStream(filename);
long end = System.currentTimeMillis();
System.out.println(Long.toHexString(crcValue));
System.out.println((end - start) + "ms");
System.out.println("Buffer Stream:");
start = System.currentTimeMillis();
crcValue = checksumBufferedInputStream(filename);
end = System.currentTimeMillis();
System.out.println(Long.toHexString(crcValue));
System.out.println((end - start) + "ms");
System.out.println("Random Access File:");
start = System.currentTimeMillis();
crcValue = checksumRandomAccessFile(filename);
end = System.currentTimeMillis();
System.out.println(Long.toHexString(crcValue));
System.out.println((end - start) + "ms");
System.out.println("Mapped File:");
start = System.currentTimeMillis();
crcValue = checksumMappedFile(filename);
end = System.currentTimeMillis();
System.out.println(Long.toHexString(crcValue));
System.out.println((end - start) + "ms");
}
示例运行结果
结果如下,可以看到运行与开始性能对比截图一致。 这也暗示着,对于追求处理速度的大文件,不建议使用InputStream和RandomAccessFile, 建议使用MappedByteBuffer或者BufferedInputStream。
Input Stream:
d6b12853
158067ms
Buffer Stream:
d6b12853
385ms
Random Access File:
d6b12853
181429ms
Mapped File:
d6b12853
203ms
文件加锁
锁定文件
使用FileChannel.lock()或者FileChannel.tryLock()。文件锁定后,将保持锁定, 直到通道关闭或者锁上调用了release()方法
FileChannel channel = FileChannel.open(path);
// 加锁方式1
FileLock lock = channel.lock(); // 会阻塞直至可获得锁
// 加锁方式2
FileLock lock = channel.tryLock(); // 立即返回, 要么返回锁, 要么不可获得锁时返回null.
锁定文件的一部分
/**
* shared: false表示这是独占锁, 锁定文件的目的是读写; true表示这是一个共享锁, 允许多个进程从文件读入, 并阻止任何进程获得独占的锁. 不是所有操作系统都支持共享锁
*/
FileLock lock(long start, long size, boolean shared)
// or
FileLock tryLock(long start, long size, boolean shared)
// 查询支持的锁类型
FileLock.isShared();
如果锁定尾部,而文件后来长度增长超过锁定部分,那么增长出来的区域是未锁定的,要想锁住所有字节,使用Long.MAX_VALUE来表示尺寸。
例如,
FileLock lock = channel.lock(0, Long.MAX_VALUE, true);
释放锁
确保操作完成时释放锁,最好使用try语句
try (FileLock lock = channel.lock()) {
access the locked file segment
}
文件加锁机制依赖于操作系统,需要注意:
- 某些系统中,文件加锁仅仅是建议。如果一个应用未得到锁,仍可以向被另一个应用并发锁定的文件执行写操作;
- 某些系统中,不能锁定一个文件的同时,将其映射到内存中;
- 文件锁是由整个Java虚拟机持有。如果2个程序是由同一个虚拟机启动,那么它们不可能每个都同时获得同一个文件上的锁。如果虚拟机已经在同一个文件上持有了另一个重叠的锁,那么这2个方法将抛出OverlapingFileLockException;
- 在一些系统中,关闭一个通道会释放由Java虚拟机持有的底层文件的所有锁。因此,同一个锁定文件上,应避免使用多个通道;
- 在网络文件系统上锁定文件是高度依赖于系统的,应尽量避免;