3.4 内存映射文件 新的FileChannel类提供了一个名为map()的方法,该方法可以在一个打开的文件和一个特殊类型的ByteBuffer之间建立一个虚拟内存映射(第一章中已经归纳了什么是内存映射文件以及它们如何同虚拟内存交互)。在FileChannel上调用map()方法会创建一个由磁盘文件支持的虚拟内存映射(virtual memory mapping)并在那块虚拟内存空间外部封装一个MappedByteBuffer对象(参见图 1-6)。 由map()方法返回的MappedByteBuffer对象的行为在多数方面类似一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘上的一个文件中。调用get()方法会从磁盘文件中获取数据,此数据反映该文件的当前内容,即使在映射建立之后文件已经被一个外部进程做了修改。通过文件映射看到的数据同您用常规方法读取文件看到的内容是完全一样的。相似地,对映射的缓冲区实现一个put()会更新磁盘上的那个文件(假设对该文件您有写的权限),并且您做的修改对于该文件的其他阅读者也是可见的。 通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。因为不需要做明确的系统调用,那会很消耗时间。更重要的是,操作系统的虚拟内存可以自动缓存内存页(memory page)。这些页是用系统内存来缓存的,所以不会消耗Java虚拟机内存堆(memory heap)。 一旦一个内存页已经生效(从磁盘上缓存进来),它就能以完全的硬件速度再次被访问而不需要再次调用系统命令来获取数据。那些包含索引以及其他需频繁引用或更新的内容的巨大而结构化文件能因内存映射机制受益非常多。如果同时结合文件锁定来保护关键区域和控制事务原子性,那您将能了解到内存映射缓冲区如何可以被很好地利用。 下面让我们来看一下如何使用内存映射: public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // 这里仅列出部分API public abstract MappedByteBuffer map(MapMode mode, long position, long size) public static class MapMode { public static final MapMode READ_ONLY public static final MapMode READ_WRITE public static final MapMode PRIVATE } } 可以看到,只有一种map()方法来创建一个文件映射。它的参数有mode,position和size。参数position和size同lock()方法的这两个参数是一样的(在前面的章节中已有讨论)。我们可以创建一个MappedByteBuffer来代表一个文件中字节的某个子范围。例如,要映射100到299(包含299)位置的字节,可以使用下面的代码: buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 100, 200); 如果要映射整个文件则使用: buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); 与文件锁的范围机制不一样,映射文件的范围不应超过文件的实际大小。如果您请求一个超出文件大小的映射,文件会被增大以匹配映射的大小。假如您给size参数传递的值是Integer.MAX_VALUE,文件大小的值会膨胀到超过2.1GB。即使您请求的是一个只读映射,map()方法也会尝试这样做并且大多数情况下都会抛出一个IOException异常,因为底层的文件不能被修改。该行为同之前讨论的文件「空洞」的行为是一致的。详情请参考 3.3.1 节。 FileChannel类定义了代表映射模式的常量,且是使用一个类型安全的枚举而非数字值来定义这些常量。这些常量是FileChannel内部定义的一个内部类(inner class)的静态字段,它们可以在编译时被检查类型,不过您可以像使用一个数值型常量那样使用它们。 同常规的文件句柄类似,文件映射可以是可写的或只读的。前两种映射模式MapMode.READ_ONLY和MapMode.READ_WRITE意义是很明显的,它们表示您希望获取的映射只读还是允许修改映射的文件。请求的映射模式将受被调用map()方法的FileChannel对象的访问权限所限制。如果通道是以只读的权限打开的而您却请求MapMode.READ_WRITE模式,那么map()方法会抛出一个NonWritableChannelException异常;如果您在一个没有读权限的通道上请求MapMode.READ_ONLY映射模式,那么将产生NonReadableChannelException异常。不过在以read/write权限打开的通道上请求一个MapMode.READ_ONLY映射却是允许的。MappedByteBuffer对象的可变性可以通过对它调用isReadOnly()方法来检查。 第三种模式MapMode.PRIVATE表示您想要一个写时拷贝(copy-on-write)的映射。这意味着您通过put()方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer实例可以看到。该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage collected),那些修改都会丢失。尽管写时拷贝的映射可以防止底层文件被修改,您也必须以read/write权限来打开文件以建立MapMode.PRIVATE映射。只有这样,返回的MappedByteBuffer对象才能允许使用put()方法。 写时拷贝这一技术经常被操作系统使用,以在一个进程生成另一个进程时管理虚拟地址空间(virtual address spaces)。使用写时拷贝可以允许父进程和子进程共享内存页直到它们中的一方实际发生修改行为。在处理同一文件的多个映射时也有相同的优势(当然,这需要底层操作系统的支持)。假设一个文件被多个MappedByteBuffer对象映射并且每个映射都是MapMode.PRIVATE模式,那么这份文件的大部分内容都可以被所有映射共享。 选择使用MapMode.PRIVATE模式并不会导致您的缓冲区看不到通过其他方式对文件所做的修改。对文件某个区域的修改在使用MapMode.PRIVATE模式的缓冲区中都能反映出来,除非该缓冲区已经修改了文件上的同一个区域。正如第一章中所描述的,内存和文件系统都被划分成了页。当在一个写时拷贝的缓冲区上调用put()方法时,受影响的页会被拷贝,然后更改就会应用到该拷贝中。具体的页面大小取决于具体实现,不过通常都是和底层文件系统的页面大小时一样的。如果缓冲区还没对某个页做出修改,那么这个页就会反映被映射文件的相应位置上的内容。一旦某个页因为写操作而被拷贝,之后就将使用该拷贝页,并且不能被其他缓冲区或文件更新所修改。例3-5 的代码诠释了这一行为。 您应该注意到了没有unmap()方法。也就是说,一个映射一旦建立之后将保持有效,直到MappedByteBuffer对象被施以垃圾收集动作为止。同锁不一样的是,映射缓冲区没有绑定到创建它们的通道上。关闭相关联的FileChannel不会破坏映射,只有丢弃缓冲区对象本身才会破坏该映射。NIO设计师们之所以做这样的决定是因为当关闭通道时破坏映射会引起安全问题,而解决该安全问题又会导致性能问题。如果您确实需要知道一个映射是什么时候被破坏的,他们建议使用虚引用(phantom references,参见java.lang.ref.PhantomReference)和一个cleanup线程。不过有此需要的概率是微乎其微的。 MemoryMappedBuffer直接反映它所关联的磁盘文件。如果映射有效时文件被在结构上修改,就会产生奇怪的行为(当然具体的行为是取决于操作系统和文件系统的)。MemoryMappedBuffer有固定的大小,不过它所映射的文件却是弹性的。具体来说,如果映射有效时文件大小变化了,那么缓冲区的部分或全部内容都可能无法访问,并将返回未定义的数据或者抛出未检查的异常。关于被内存映射的文件如何受其他线程或外部进程控制这一点,请务必小心对待。 所有的MappedByteBuffer对象都是直接的,这意味着它们占用的内存空间位于Java虚拟机内存堆之外(并且可能不会算作 Java 虚拟机的内存占用,不过这取决于操作系统的虚拟内存模型)。 因为MappedByteBuffers也是ByteBuffers,所以能够被传递SocketChannel之类通道的read()或write()以有效传输数据给被映射的文件或从被映射的文件读取数据。如能再结合scatter/gather,那么从内存缓冲区和被映射文件内容中组织数据就变得很容易了。例 3-4 就是以此方式写 HTTP 回应的。3.4.1节中将描述一个传输数据给通道或从其他通道读取数据的更加有效的方式。 到现在为止,我们已经讨论完了映射缓冲区同其他缓冲区相同的特性,这些也是您会用得最多的。不过MappedByteBuffer还定义了几个它独有的方法: public abstract class MappedByteBuffer extends ByteBuffer { // 这里仅列出部分API public final MappedByteBuffer load() public final boolean isLoaded() public final MappedByteBuffer force() } 当我们为一个文件建立虚拟内存映射之后,文件数据通常不会因此被从磁盘读取到内存(这取决于操作系统)。该过程类似打开一个文件:文件先被定位,然后一个文件句柄会被创建,当您准备好之后就可以通过这个句柄来访问文件数据。对于映射缓冲区,虚拟内存系统将根据您的需要来把文件中相应区块的数据读进来。这个页验证或防错过程需要一定的时间,因为将文件数据读取到内存需要一次或多次的磁盘访问。某些场景下,您可能想先把所有的页都读进内存以实现最小的缓冲区访问延迟。如果文件的所有页都是常驻内存的,那么它的访问速度就和访问一个基于内存的缓冲区一样了。 load()方法会加载整个文件以使它常驻内存。正如我们在第一章所讨论的,一个内存映射缓冲区会建立与某个文件的虚拟内存映射。此映射使得操作系统的底层虚拟内存子系统可以根据需要将文件中相应区块的数据读进内存。已经在内存中或通过验证的页会占用实际内存空间,并且在它们被读进RAM时会挤出最近较少使用的其他内存页。 在一个映射缓冲区上调用load()方法会是一个代价高的操作,因为它会导致大量的页调入(page-in),具体数量取决于文件中被映射区域的实际大小。然而,load()方法返回并不能保证文件就会完全常驻内存,这是由于请求页面调入(demand paging)是动态的。具体结果会因某些因素而有所差异,这些因素包括:操作系统、文件系统,可用Java虚拟机内存,最大Java虚拟机内存,垃圾收集器实现过程等等。请小心使用load()方法,它可能会导致您不希望出现的结果。该方法的主要作用是为提前加载文件埋单,以便后续的访问速度可以尽可能的快。 对于那些要求近乎实时访问(near-realtime access)的程序,解决方案就是预加载。但是请记住,不能保证全部页都会常驻内存,不管怎样,之后可能还会有页调入发生。内存页什么时候以及怎样消失受多个因素影响,这些因素中的许多都是不受 Java 虚拟机控制的。JDK 1.4的NIO并没有提供一个可以把页面固定到物理内存上的 API,尽管一些操作系统是支持这样做的。 对于大多数程序,特别是交互性的或其他事件驱动(event-driven)的程序而言,为提前加载文件消耗资源是不划算的。在实际访问时分摊页调入开销才是更好的选择。让操作系统根据需要来调入页意味着不访问的页永远不需要被加载。同预加载整个被映射的文件相比,这很容易减少 I/O 活动总次数。操作系统已经有一个复杂的内存管理系统了,就让它来替您完成此工作吧! 我们可以通过调用isLoaded()方法来判断一个被映射的文件是否完全常驻内存了。如果该方法返回true值,那么很大概率是映射缓冲区的访问延迟很少或者根本没有延迟。不过,这也是不能保证的。同样地,返回false值并不一定意味着访问缓冲区将很慢或者该文件并未完全常驻内存。isLoaded()方法的返回值只是一个暗示,由于垃圾收集的异步性质、底层操作系统以及运行系统的动态性等因素,想要在任意时刻准确判断全部映射页的状态是不可能的。 上面代码中列出的最后一个方法force()同FileChannel类中的同名方法相似(参见 3.3.1 节)该方法会强制将映射缓冲区上的更改应用到永久磁盘存储器上。当用MappedByteBuffer对象来更新一个文件,您应该总是使用MappedByteBuffer.force()而非FileChannel.force(),因为通道对象可能不清楚通过映射缓冲区做出的文件的全部更改。MappedByteBuffer没有不更新文件元数据的选项——元数据总是会同时被更新的。请注意,非本地文件系统也同样影响MappedByteBuffer.force()方法,正如它会对FileChannel.force()方法有影响,在这里(参见 3.3.1 节)。 如果映射是以MapMode.READ_ONLY或MAP_MODE.PRIVATE模式建立的,那么调用force()方法将不起任何作用,因为永远不会有更改需要应用到磁盘上(但是这样做也是没有害处的)。例 3-4 诠释了内存映射缓冲区如何同scatter/gather结合使用。 /* *例 3-4 使用映射文件和 gathering 写操作来编写 HTTP 回复 */ package com.ronsoft.books.nio.channels; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; import java.nio.channels.GatheringByteChannel; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.URLConnection; /** * Dummy HTTP server using MappedByteBuffers. * Given a filename on the command line, pretend to be * a web server and generate an HTTP response containing * the file content preceded by appropriate headers. The * data is sent with a gathering write. * * @author Ron Hitchens (ron@ronsoft.com) */ public class MappedHttp { private static final String OUTPUT_FILE = "MappedHttp.out"; private static final String LINE_SEP = "\r\n"; private static final String SERVER_ID = "Server: Ronsoft Dummy Server"; private static final String HTTP_HDR = "HTTP/1.0 200 OK" LINE_SEP SERVER_ID LINE_SEP; private static final String HTTP_404_HDR = "HTTP/1.0 404 Not Found" LINE_SEP SERVER_ID LINE_SEP; private static final String MSG_404 = "Could not open file: "; public static void main (String [] argv) throws Exception { if (argv.length < 1) { System.err.println ("Usage: filename"); return; } String file = argv [0]; ByteBuffer header = ByteBuffer.wrap(bytes (HTTP_HDR)); ByteBuffer dynhdrs = ByteBuffer.allocate(128); ByteBuffer [] gather = { header, dynhdrs, null }; String contentType = "unknown/unknown"; long contentLength = -1; try { FileInputStream fis = new FileInputStream (file); FileChannel fc = fis.getChannel(); MappedByteBuffer filedata = fc.map (MapMode.READ_ONLY, 0, fc.size()); gather [2] = filedata; contentLength = fc.size(); contentType = URLConnection.guessContentTypeFromName (file); } catch (IOException e) { // file could not be opened; report problem ByteBuffer buf = ByteBuffer.allocate (128); String msg = MSG_404 e LINE_SEP; buf.put (bytes (msg)); buf.flip(); // Use the HTTP error response gather [0] = ByteBuffer.wrap (bytes (HTTP_404_HDR)); gather [2] = buf; contentLength = msg.length(); contentType = "text/plain"; } StringBuffer sb = new StringBuffer(); sb.append ("Content-Length: " contentLength); sb.append (LINE_SEP); sb.append ("Content-Type: ").append (contentType); sb.append (LINE_SEP).append (LINE_SEP); dynhdrs.put (bytes (sb.toString())); dynhdrs.flip(); FileOutputStream fos = new FileOutputStream (OUTPUT_FILE); FileChannel out = fos.getChannel(); // All the buffers have been prepared; write 'em out while (out.write (gather) > 0) { // Empty body; loop until all buffers are empty } out.close(); System.out.println ("output written to " OUTPUT_FILE); } // Convert a string to its constituent bytes // from the ASCII character set private static byte[] bytes(String string) throws Exception { return (string.getBytes ("US-ASCII")); } } 例 3-5 诠释了各种模式的内存映射如何交互。具体来说,例中代码诠释了写时拷贝是如何页导向(page-oriented)的。当在使用MAP_MODE.PRIVATE模式创建的MappedByteBuffer对象上调用put()方法而引发更改时,就会生成一个受影响页的拷贝。这份私有的拷贝不仅反映本地更改,而且使缓冲区免受来自外部对原来页更改的影响。然而,对于被映射文件其他区域的更改还是可以看到的。 /* *例 3-5 三种类型的内存映射缓冲区 */ package com.ronsoft.books.nio.channels; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.io.File; import java.io.RandomAccessFile; /** * Test behavior of Memory mapped buffer types. Create a file, write * some data to it, then create three different types of mappings * to it. Observe the effects of changes through the buffer APIs * and updating the file directly. The data spans page boundaries * to illustrate the page-oriented nature of Copy-On-Write mappings. * * @author Ron Hitchens (ron@ronsoft.com) */ public class MapFile { public static void main (String [] argv) throws Exception { // Create a temp file and get a channel connected to it File tempFile = File.createTempFile ("mmaptest", null); RandomAccessFile file = new RandomAccessFile (tempFile, "rw"); FileChannel channel = file.getChannel(); ByteBuffer temp = ByteBuffer.allocate (100); // Put something in the file, starting at location 0 temp.put ("This is the file content".getBytes()); temp.flip(); channel.write (temp, 0); // Put something else in the file, starting at location 8192. // 8192 is 8 KB, almost certainly a different memory/FS page. // This may cause a file hole, depending on the // filesystem page size. temp.clear(); temp.put ("This is more file content".getBytes()); temp.flip(); channel.write (temp, 8192); // Create three types of mappings to the same file MappedByteBuffer ro = channel.map ( FileChannel.MapMode.READ_ONLY, 0, channel.size()); MappedByteBuffer rw = channel.map ( FileChannel.MapMode.READ_WRITE, 0, channel.size()); MappedByteBuffer cow = channel.map ( FileChannel.MapMode.PRIVATE, 0, channel.size()); // the buffer states before any modifications System.out.println ("Begin"); showBuffers (ro, rw, cow); // Modify the copy-on-write buffer cow.position (8); cow.put ("COW".getBytes()); System.out.println ("Change to COW buffer"); showBuffers (ro, rw, cow); // Modify the read/write buffer rw.position (9); rw.put (" R/W ".getBytes()); rw.position (8194); rw.put (" R/W ".getBytes()); rw.force(); System.out.println ("Change to R/W buffer"); showBuffers (ro, rw, cow); // Write to the file through the channel; hit both pages temp.clear(); temp.put ("Channel write ".getBytes()); temp.flip(); channel.write (temp, 0); temp.rewind(); channel.write (temp, 8202); System.out.println ("Write on channel"); showBuffers (ro, rw, cow); // Modify the copy-on-write buffer again cow.position (8207); cow.put (" COW2 ".getBytes()); System.out.println ("Second change to COW buffer"); showBuffers (ro, rw, cow); // Modify the read/write buffer rw.position (0); rw.put (" R/W2 ".getBytes()); rw.position (8210); rw.put (" R/W2 ".getBytes()); rw.force(); System.out.println ("Second change to R/W buffer"); showBuffers (ro, rw, cow); // cleanup channel.close(); file.close(); tempFile.delete(); } // Show the current content of the three buffers public static void showBuffers(ByteBuffer ro, ByteBuffer rw, ByteBuffer cow) throws Exception { dumpBuffer ("R/O", ro); dumpBuffer ("R/W", rw); dumpBuffer ("COW", cow); System.out.println (""); } // Dump buffer content, counting and skipping nulls public static void dumpBuffer (String prefix, ByteBuffer buffer) throws Exception { System.out.print (prefix ": '"); int nulls = 0; int limit = buffer.limit(); for (int i = 0; i < limit; i ) { char c = (char) buffer.get (i); if (c == '\u0000') { nulls ; continue; } if (nulls != 0) { System.out.print ("|[" nulls " nulls]|"); nulls = 0; } System.out.print(c); } System.out.println ("'"); } } 以下是运行上面程序的输出: BeginR/O: 'This is the file content|[8168 nulls]|This is more file content'R/W: 'This is the file content|[8168 nulls]|This is more file content'COW: 'This is the file content|[8168 nulls]|This is more file content'Change to COW bufferR/O: 'This is the file content|[8168 nulls]|This is more file content'R/W: 'This is the file content|[8168 nulls]|This is more file content'COW: 'This is COW file content|[8168 nulls]|This is more file content'Change to R/W bufferR/O: 'This is t R/W le content|[8168 nulls]|Th R/W more file content'R/W: 'This is t R/W le content|[8168 nulls]|Th R/W more file content'COW: 'This is COW file content|[8168 nulls]|Th R/W more file content'Write on channelR/O: 'Channel write le content|[8168 nulls]|Th R/W moChannel write t'R/W: 'Channel write le content|[8168 nulls]|Th R/W moChannel write t'COW: 'This is COW file content|[8168 nulls]|Th R/W moChannel write t'Second change to COW bufferR/O: 'Channel write le content|[8168 nulls]|Th R/W moChannel write t'R/W: 'Channel write le content|[8168 nulls]|Th R/W moChannel write t'COW: 'This is COW file content|[8168 nulls]|Th R/W moChann COW2 te t'Second change to R/W bufferR/O: ' R/W2 l write le content|[8168 nulls]|Th R/W moChannel R/W2 t'R/W: ' R/W2 l write le content|[8168 nulls]|Th R/W moChannel R/W2 t'COW: 'This is COW file content|[8168 nulls]|Th R/W moChann COW2 te t' Java nio入门教程详解(二十二) 00 我们认为:用户的主要目的,是为了获取有用的信息,而不是来点击广告的。因此本站将竭力做好内容,并将广告和内容进行分离,确保所有广告不会影响到用户的正常阅读体验。用户仅凭个人意愿和兴趣爱好点击广告。 我们坚信:只有给用户带来价值,用户才会给我们以回报。 |
|