Java Nio Buffer 十二

2017/05/25 Java

Java Nio Buffer 十二

内存映射文件

新的 FileChannel 类提供了一个名为 map( )的方法,该方法可以在一个打开的文件和一个特殊 类型的 ByteBuffer 之间建立一个虚拟内存映射。在 FileChannel 上调用 map( )方法会创建一个由磁盘文件支持的虚拟内存映射(virtual memory mapping)并在那块虚拟内存空间外部封装一个 MappedByteBuffer 对象。

map( )方法返回的 MappedByteBuffer 对象的行为在多数方面类似一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘上的一个文件中。调用 get( )方法会从磁盘文件中获取数据,此数据反映该文件的当前内容,即使在映射建立之后文件已经被一个外部进程做了修改。通过文件映射看到的数据和使用常规方法读取文件看到的内容是完全一样的。对映射的缓冲区实现一个 put( )会更新磁盘上的那个文件,并且修改对于该文件的其他程序也是可见的。

通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。因为不需要做明确的系统调用,那会很消耗时间。更重要的是,操作系统的虚拟内存可以自动缓存内存页(memory page)。这些页是用系统内存来缓存的,所以不会消耗 Java 虚拟机内存堆 (memory heap)。

一旦一个内存页已经生效(从磁盘上缓存进来),它就能以完全的硬件速度再次被访问而不需要再次调用系统命令来获取数据。那些包含索引以及其他需频繁引用或更新的内容的巨大而结构化文件能因内存映射机制受益非常多。

内存映射API如下:

public abstract class FileChannel
    extends AbstractChannel
    implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
    // This is a partial API listing
    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
    } 
}

两个示例如下:


buffer = fileChannel.map (FileChannel.MapMode.READ_ONLY, 100, 200);

buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

与文件锁的范围机制不一样,映射文件的范围不应超过文件的实际大小。如果请求一个超出文件大小的映射,文件会被增大以匹配映射的大小。如果请求的是一个只读映射,会产生IO异常。

MapMode的第三种会产生一个写时拷贝(copy-on-write)的映射。这意味着通过 put( )方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有 MappedByteBuffer 实例可以看到。该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage collected),那些修改都会丢失。尽管写时拷贝的映射可以防止底层文件被修改,也必须以 read/write 权限来打开文件以建立 MapMode.PRIVATE 映射。只有这样,返回的 MappedByteBuffer 对象才能允许使用 put( )方法。

一个映射一旦建立之后将保持有效,直到 MappedByteBuffer 对象被施以垃圾收集动作为止。

MemoryMappedBuffer 直接反映它所关联的磁盘文件。如果映射有效时文件被在结构上修改, 就会产生奇怪的行为(当然具体的行为是取决于操作系统和文件系统的)。MemoryMappedBuffer 有固定的大小,不过它所映射的文件却是弹性的。如果映射有效时文件大小变化了,会导致缓冲区的部分或全部内容都可能无法访问,并将返回未定义的数据或者抛出未检查的异常。

所有的 MappedByteBuffer 对象都是直接的,这意味着它们占用的内存空间位于 Java 虚拟机内存堆之外(并且可能不会算作 Java 虚拟机的内存占用,不过这取决于操作系统的虚拟内存模型)。

MappedByteBuffers 也是 ByteBuffers,所以能够被传递 SocketChannel 之类通道的 read( )write( )以有效传输数据给被映射的文件或从被映射的文件读取数据。如能再结合 scatter/gather,从内存缓冲区和被映射文件内容中组织数据就变得很容易。

MappedByteBuffer 还定义了几个它独有的方法:


public abstract class MappedByteBuffer
    extends ByteBuffer
{
    // This is a partial API listing
    public final MappedByteBuffer load( )
    public final boolean isLoaded( )
    public final MappedByteBuffer force( )
}

load( )方法会加载整个文件以使它常驻内存。在一个映射缓冲区上调用 load( )方法会是一个代价高的操作,因为它会导致大量的页调入 (page-in),具体数量取决于文件中被映射区域的实际大小。load( )方法返回并不能保证文件就会完全常驻内存,这是由于请求页面调入(demand paging)是动态的。该方法的主要作用是为提前加载文件,以便后续的访问速度可以尽可能的快。

对于那些要求近乎实时访问(near-realtime access)的程序,解决方案就是预加载。不能保证全部页都会常驻内存。

通过调用 isLoaded( )方法来判断一个被映射的文件是否完全常驻内存了。如果该方法 返回 true 值,那么很大概率是映射缓冲区的访问延迟很少或者根本没有延迟。这也是不能保证的。isLoaded( )方法的返回值只是一个暗示。

force( )同 FileChannel 类中的同名方法相似,该方法会强制将映射缓冲区上的更改应用到永久磁盘存储器上。如果映射是以 MapMode.READ_ONLY MAP_MODE.PRIVATE 模式建立的,那么调用 force( ) 方法将不起任何作用,因为不会有更改需要应用到磁盘上,也不会带来副作用。

## Channel-to-Channel 传输

FileChannel 类添加了一些优化方法来提高从通道到通道直接的传输效率。

public abstract class FileChannel
    extends AbstractChannel
    implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
    // This is a partial API listing
    public abstract long transferTo (long position, long count,WritableByteChannel target)
    public abstract long transferFrom (ReadableByteChannel src,long position, long count)
}

transferTo( )transferFrom( )方法允许将一个通道交叉连接到另一个通道,而不需要通过一个 中间缓冲区来传递数据。只有 FileChannel 类有这两个方法,因此 channel-to-channel 传输中通道之一必须是 FileChannel。

直接的通道传输不会更新与某个 FileChannel 关联的 position 值。请求的数据传输将从 position 参数指定的位置开始,传输的字节数不超过 count 参数的值。实际传输的字节数会由方法返回,可能少于请求的字节数。

对于传输数据来源是一个文件的 transferTo( )方法,如果 position + count 的值大于文件的 size 值,传输会在文件尾的位置终止。

Channel-to-channel 传输是可以极其快速的,特别是在底层操作系统提供本地支持的时候。某些操作系统可以不必通过用户空间传递数据而进行直接的数据传输。

Show Disqus Comments

Search

    Table of Contents