Java 内存映射文件 – Java MappedByteBuffer

了解 Java 内存映射文件,并借助RandomAccessFileMemoryMappedBuffer从内存映射文件中读取和写入内容。

1. 内存映射的 IO

如果您知道 Java IO 在较低级别的工作方式,那么您将了解缓冲区处理,内存分页和其他此类概念。 对于传统的文件 I/O,用户进程在其中发出read()write()系统调用来传输数据,几乎总是存在一个或多个复制操作,以在内核空间中的这些文件系统页面和内存中的内存区域之间移动数据。 用户空间。 这是因为文件系统页面和用户缓冲区之间通常没有一对一的对齐方式。

但是,大多数操作系统都支持一种特殊类型的 I/O 操作,它允许用户进程最大程度地利用系统 I/O 的面向页面的特性,并完全避免缓冲区复制。 这称为内存映射的 I/O ,我们将在此处学习有关内存映射文件的一些知识。

2. Java 内存映射文件

内存映射的 I/O 使用文件系统来建立从用户空间直接到适用文件系统页面的虚拟内存映射。 使用内存映射文件,我们可以假装整个文件都在内存中,并且可以通过简单地将其视为非常大的数组来访问它。 这种方法极大地简化了我们为修改文件而编写的代码。

阅读更多信息:使用缓冲区

为了在内存映射文件中进行写入和读取,我们从RandomAccessFile开始,获取该文件的通道。 存储器映射的字节缓冲区是通过FileChannel.map()方法创建的。 此类通过特定于内存映射文件区域的操作扩展了ByteBuffer类。

映射的字节缓冲区及其表示的文件映射将保持有效,直到缓冲区本身被垃圾回收为止。 请注意,必须指定文件中要映射的区域的起点和长度。 这意味着您可以选择映射大文件的较小区域。

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MemoryMappedFileExample 
{
	static int length = 0x8FFFFFF; 	

	public static void main(String[] args) throws Exception 
	{
		try(RandomAccessFile file = new RandomAccessFile("howtodoinjava.dat", "rw")) 
		{
			MappedByteBuffer out = file.getChannel()
										.map(FileChannel.MapMode.READ_WRITE, 0, length);

			for (int i = 0; i < length; i++) 
			{
				out.put((byte) 'x');
			}

			System.out.println("Finished writing");
		}
	}
}

使用上述程序创建的文件的长度为 128 MB,可能大于操作系统允许的空间。 该文件似乎可以一次访问,因为只有部分文件被带入内存,而其他部分被换出。 这样,可以轻松地修改非常大的文件(最大 2 GB)。

文件模式

像常规文件句柄一样,文件映射可以是可写的或只读的。

  • 前两个映射模式MapMode.READ_ONLY** 和 **MapMode.READ_WRITE相当明显。 它们指示您是要映射为只读映射还是允许修改映射文件。
  • 第三种模式MapMode.PRIVATE表示您需要写时复制映射。 这意味着您通过put()进行的任何修改都将导致只有MappedByteBuffer实例可以看到的数据的私有副本。 不会对基础文件进行任何更改,并且对缓冲区进行垃圾回收时所做的任何更改都将丢失。 即使写时复制映射阻止了对基础文件的任何更改,您也必须已打开文件以进行读/写以设置MapMode.PRIVATE映射。 这对于返回的MappedByteBuffer对象允许put()是必需的。

您会注意到,没有unmap()方法。 建立后,映射将一直有效,直到MappedByteBuffer对象被垃圾回收为止。 同样,映射缓冲区不绑定到创建它们的通道。 关闭关联的FileChannel不会破坏映射。 仅处理缓冲区对象本身会破坏映射。

MemoryMappedBuffer具有固定大小,但映射到的文件是弹性的。 具体来说,如果在映射生效时文件大小发生变化,则部分或全部缓冲区可能变得不可访问,可能会返回未定义的数据,或者会引发未经检查的异常。 内存映射文件时,请注意其他线程或外部进程如何处理文件。

3. 内存映射文件的好处

与常规 I/O 相比,内存映射 IO 具有以下优点:

  1. 用户进程将文件数据视为内存,因此无需发出read()write()系统调用。
  2. 当用户进程触摸映射的内存空间时,将自动生成页面错误,以从磁盘引入文件数据。 如果用户修改了映射的内存空间,则受影响的页面会自动标记为脏页面,随后将刷新到磁盘以更新文件。
  3. 操作系统的虚拟内存子系统将执行页面的智能缓存,并根据系统负载自动管理内存。
  4. 数据总是页面对齐的,不需要缓冲区复制。
  5. 可以映射非常大的文件,而无需消耗大量内存来复制数据。

4. 如何读取内存映射文件

要使用内存映射的 IO 读取文件,请使用以下代码模板:

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MemoryMappedFileReadExample 
{
	private static String bigExcelFile = "bigFile.xls";

	public static void main(String[] args) throws Exception 
	{
		try (RandomAccessFile file = new RandomAccessFile(new File(bigExcelFile), "r"))
		{
			//Get file channel in read-only mode
			FileChannel fileChannel = file.getChannel();

	        //Get direct byte buffer access using channel.map() operation
	        MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

	        // the buffer now reads the file as if it were loaded in memory. 
	        System.out.println(buffer.isLoaded()); 	//prints false
	        System.out.println(buffer.capacity());	//Get the size based on content size of file

	        //You can read the file from this buffer the way you like.
	        for (int i = 0; i < buffer.limit(); i++)
	        {
	            System.out.print((char) buffer.get()); //Print the content of file
	        }
		}
	}
}

5. 如何写入内存映射文件

要使用内存映射的 IO 将数据写入文件,请使用以下代码模板:

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MemoryMappedFileWriteExample {
	private static String bigTextFile = "test.txt";

	public static void main(String[] args) throws Exception 
	{
		// Create file object
		File file = new File(bigTextFile);

		//Delete the file; we will create a new file
		file.delete();

		try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"))
		{
			// Get file channel in read-write mode
			FileChannel fileChannel = randomAccessFile.getChannel();

			// Get direct byte buffer access using channel.map() operation
			MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8 * 8);

			//Write the content using put methods
			buffer.put("howtodoinjava.com".getBytes());
		}
	}
}

在评论部分中将您的评论和想法留给我。