内存映射文件(Memory-Mapped File)
假设需要对一个文件的内容频繁访问,甚至要做修改,如何才能高效访问?再者,修改后要让别人也能及时看到该文件的新内容,如何做到?对于前者,传统的做法是“打开文件,读入”,为了提高效率,可以将整个文件内容读入到内存,之后的读从内存取即可;对于后者,需要调用者做一些同步操作来保证一致性。可见,传统做法需要将文件内容都读入内存而无法按需只读入需要的内容(虽RandmonAccessFile支持读部分内容,但“按需”加载需要的内容到内存的操作仍需要由开发人员负责),且文件读写的API自身无法保证内容一致性。
内存映射文件(Memory-Mapped File)就可以解决上述痛点。
是什么
内存映射文件是OS提供的一种高效的文件IO技术。内存映射文件,见名知意,是一种文件且文件内容被映射到内存了,使用者可以通过内存读写来读写该文件。其效果是使得使用者可以以像读写内存数据一样的方式读写磁盘文件,其主要特点:
效率高:数据映射到内存进行操作(由OS负责在恰当时机将数据同步到磁盘)故效率高;且映射到OS内核态内存而不是语言自身的内存(如Java堆内存)故少了一些数据复制操作。
自动按需映射:自动将文件内容映射到内存、且可以做到“按需”映射——只将文件的部分内容映射到内存。
多映射共享:同一个文件可被多个进程映射到各自内存,各进程看到的内容一致,即该技术自身做到了“同步”。
可见,内存映射文件从功能上看与传统做法的功能无异,但效果上却大不相同,体现在:文件内容加载、按需加载、同步等全由内存映射文件自身支持而不需要开发者介入;效率更高。
适用场景
内存映射文件的主要使用场景是 需要频繁读取的文件数据、文件共享、进程通信 等。
注:
各种语言对内存映射文件的支持实际上就是进行了一层封装,最终仍是利用OS提供的能力(例如Linux的 mmap())。
Java Memory-Mapped File所使用的内存分配在物理内存而不是JVM堆内存,且分配在OS内核。
1:
内存映射文件及其应用 - 实现一个简单的消息队列 / 计算机程序的思维逻辑
在一般的文件读写中,会有两次数据拷贝,一次是从硬盘拷贝到操作系统内核,另一次是从操作系统内核拷贝到用户态的应用程序。而在内存映射文件中,一般情况下,只有一次拷贝,且内存分配在操作系统内核,应用程序访问的就是操作系统的内核内存空间,这显然要比普通的读写效率更高。
内存映射文件的另一个重要特点是,它可以被多个不同的应用程序共享,多个程序可以映射同一个文件,映射到同一块内存区域,一个程序对内存的修改,可以让其他程序也看到,这使得它特别适合用于不同应用程序之间的通信。比普通的基于loopback接口的Socket要快10倍。
简单总结下,对于一般的文件读写不需要使用内存映射文件,但如果处理的是大文件,要求极高的读写效率,比如数据库系统或繁忙的电子交易系统,或者需要在不同程序间进行共享和通信,那就可以考虑内存映射文件。
2、
为何要在Java中使用内存映射文件(Memory Mapped File)或者MappedByteBuffer
1)Java语言通过java.nio包支持内存映射文件和IO。
2)内存映射文件用于对性能要求高的系统中,如繁忙的电子交易系统
3)使用内存映射IO你可以将文件的一部分加载到内存中
4)如果被请求的页面不在内存中,内存映射文件会导致页面错误
5)将一个文件区间映射到内存中的能力取决于内存的可寻址范围。在32位机器中,不能超过4GB,即2^32比特。
6)Java中的内存映射文件比流IO要快(注:对于大文件而言是对的,小文件则未必)
7)用于加载文件的内存在Java的堆内存之外,存在于共享内存中,允许两个不同进程访问文件。顺便说一下,这依赖于你用的是direct还是non-direct字节缓存。
8)读写内存映射文件是操作系统来负责的,因此,即使你的Java程序在写入内存后就挂掉了,只要操作系统工作正常,数据就会写入磁盘。
9)Direct字节缓存比non-direct字节缓存性能要好
10)不要经常调用MappedByteBuffer.force()方法,这个方法强制操作系统将内存中的内容写入硬盘,所以如果你在每次写内存映射文件后都调用force()方法,你就不能真正从内存映射文件中获益,而是跟disk IO差不多。
11)如果电源故障或者主机瘫痪,有可能内存映射文件还没有写入磁盘,意味着可能会丢失一些关键数据。
12)MappedByteBuffer和文件映射在缓存被GC之前都是有效的。sun.misc.Cleaner可能是清除内存映射文件的唯一选择。
3、Java内存映射文件
Java NIO的 FileChannel 类提供了一个名为 map( )的方法,该方法将一个打开的文件和一个特殊类型的 ByteBuffer 之间建立一个虚拟内存映射,由 map( )方法返回的 MappedByteBuffer 对象的行为类似于基于堆内存的ByteBuffer对象,只不过该对象的数据存储在磁盘上的文件中。通过内存映射机制来访问一个文件会比使用常规java.io 中方法的读写高效得多,甚至比使用FileChannel的普通读写方法效率都高。主要优点:效率高、支持部分映射、进程共享。
映射方法: buffer = fileChannel.map(MapMode.READ_WRITE, 0, fileChannel.size());
- 映射模式:MapMode.READ_WRITE、MapMode.READ_ONLY、MapMode.PRIVATE
- 请求的映射模式将受被调用 map( )方法的 FileChannel 对象的访问权限所限制。如:若通道以只读的权限打开的却请求 MapMode.READ_WRITE 模式,则map( )方法会抛出一个 NonWritableChannelException 异常
- MapMode.PRIVATE模式表示一个写时拷贝( copy-on-write)的映射,这意味着通过 put( )方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer 实例可以看到。该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作( garbage collected),那些修改都会丢失。
示例:通过内存映射文件简单实现持久化消息队列:
1 public static void main(String[] args) throws IOException, InterruptedException { 2 // TODO Auto-generated method stub 3 BasicQueue basicQueue = new BasicQueue("src/cn/edu/buaa/mmap", "mmap_queque"); 4 if (args.length == 1 && args[0].equals("producer")) { 5 Scanner sc = new Scanner(System.in); 6 while (sc.hasNext()) { 7 basicQueue.enqueue(sc.nextLine().getBytes()); 8 } 9 } else { 10 while (true) { 11 byte[] data = basicQueue.dequeue(); 12 if (null != data) { 13 System.out.println(new String(data)); 14 } else { 15 System.out.println(data); 16 } 17 Thread.sleep(1000); 18 } 19 } 20 } 21 22 } 23 24 25 class BasicQueue {// 为简化起见,我们暂不考虑由于并发访问等引起的一致性问题。 26 // 队列最多消息个数,实际个数还会减1 27 private static final int MAX_MSG_NUM = 1024; 28 29 // 消息体最大长度 30 private static final int MAX_MSG_BODY_SIZE = 20; 31 32 // 每条消息占用的空间 33 private static final int MSG_SIZE = MAX_MSG_BODY_SIZE + 4; 34 35 // 队列消息体数据文件大小 36 private static final int DATA_FILE_SIZE = MAX_MSG_NUM * MSG_SIZE; 37 38 // 队列元数据文件大小 (head + tail) 39 private static final int META_SIZE = 8; 40 41 private MappedByteBuffer dataBuf; 42 private MappedByteBuffer metaBuf; 43 44 public BasicQueue(String path, String queueName) throws IOException { 45 if (!path.endsWith(File.separator)) { 46 path += File.separator; 47 } 48 System.out.println(path); 49 RandomAccessFile dataFile = null; 50 RandomAccessFile metaFile = null; 51 try { 52 dataFile = new RandomAccessFile(path + queueName + ".data", "rw"); 53 metaFile = new RandomAccessFile(path + queueName + ".meta", "rw"); 54 55 dataBuf = dataFile.getChannel().map(MapMode.READ_WRITE, 0, DATA_FILE_SIZE); 56 metaBuf = metaFile.getChannel().map(MapMode.READ_WRITE, 0, META_SIZE); 57 } finally { 58 if (dataFile != null) { 59 dataFile.close(); 60 } 61 if (metaFile != null) { 62 metaFile.close(); 63 } 64 } 65 } 66 67 public void enqueue(byte[] data) throws IOException { 68 if (data.length > MAX_MSG_BODY_SIZE) { 69 throw new IllegalArgumentException( 70 "msg size is " + data.length + ", while maximum allowed length is " + MAX_MSG_BODY_SIZE); 71 } 72 if (isFull()) { 73 throw new IllegalStateException("queue is full"); 74 } 75 int tail = tail(); 76 dataBuf.position(tail); 77 dataBuf.putInt(data.length); 78 dataBuf.put(data); 79 80 if (tail + MSG_SIZE >= DATA_FILE_SIZE) { 81 tail(0); 82 } else { 83 tail(tail + MSG_SIZE); 84 } 85 } 86 87 public byte[] dequeue() throws IOException { 88 if (isEmpty()) { 89 return null; 90 } 91 int head = head(); 92 dataBuf.position(head); 93 int length = dataBuf.getInt(); 94 byte[] data = new byte[length]; 95 dataBuf.get(data); 96 97 if (head + MSG_SIZE >= DATA_FILE_SIZE) { 98 head(0); 99 } else { 100 head(head + MSG_SIZE); 101 } 102 return data; 103 } 104 105 private int head() { 106 return metaBuf.getInt(0); 107 } 108 109 private void head(int newHead) { 110 metaBuf.putInt(0, newHead); 111 } 112 113 private int tail() { 114 return metaBuf.getInt(4); 115 } 116 117 private void tail(int newTail) { 118 metaBuf.putInt(4, newTail); 119 } 120 121 private boolean isEmpty() { 122 return head() == tail(); 123 } 124 125 private boolean isFull() { 126 return ((tail() + MSG_SIZE) % DATA_FILE_SIZE) == head(); 127 } 128 }
4、使用细节
在OS层面,mmap映射的大小必须是物理页大小(通常是4KB)的整数倍,因为内存实际数据读写的单位是页(也是PageCache的页)。
若文件的实际大小小于映射区域的大小,则不足一页的文件部分仍会映射成一页内存,对内存中多出来的这部分的写不会写入到文件中。当然,如果之后文件大小扩张了,则这部分内存区域就可能变成有对应的文件内容。
对内存中超过映射区域部分的访问会触发SIGSEGV信号。
内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法访问在当前文件大小以内且在内存映射区以内的那些数据。也就是说,如果文件的大小一直在扩张,只要在映射区域范围内的数据,进程都可以合法得到,这和映射建立时文件的大小无关。
映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时可用于进程间通信的有效地址空间不完全受限于被映射文件的大小,因为是按页映射。
示例: