您的位置:首页 > 编程语言 > Java开发

Java nio 之 内存映射文件

2014-05-16 15:36 197 查看
本文整理自《Java NIO》一书。

所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件RAM)内存地址。这样做好处颇多,总结起来可分为两大类:
1. 一个以上的虚拟地址可指向同一个物理内存地址。

2. 虚拟内存空间可大于实际可用的硬件内存。

设备控制器不能通过 DMA 直接存储到用户空间,但通过利用上面提到的第一项,则可以达到相同效果。把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,这样,DMA 硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区,如下图:



这样就省去了内核与用户空间的往来拷贝,但前提条件是,内核与用户缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制器块大小(通常为 512 字节磁盘扇区)的倍数。

传统的文件 I/O 是通过用户进程发布 read( )和 write( )系统调用来传输数据的。为了在内核空间的文件系统页与用户空间的内存区之间移动数据,一次以上的拷贝操作几乎总是免不了的。这是因为,在文件系统页与用户缓冲区之间往往没有一一对应关系。但是,还有一种大多数操作系统都支持的特殊类型的 I/O 操作,允许用户进程最大限度地利用面向页的系统 I/O 特性,并完全摒弃缓冲区拷贝。这就是内存映射
I/O:



内存映射 I/O 使用文件系统建立从用户空间直到可用文件系统页的虚拟内存映射。这样做有几个好处:

•   用户进程把文件数据当作内存,所以无需发布 read( )或 write( )系统调用。

•   用户进程碰触到映射内存空间,页错误会自动产生,从而将文件数据从磁盘读进内存。如果用户修改了映射内存空间,相关页会自动标记为脏,随后刷新到磁盘,文件得到更新。

•  操作系统的虚拟内存子系统会对页进行智能高速缓存,自动根据系统负载进行内存管理。

•  数据总是按页对齐的,无需执行缓冲区拷贝。

•  大型文件使用映射,无需耗费大量内存,即可进行数据拷贝。

虚拟内存和磁盘 I/O 是紧密关联的,从很多方面看来,它们只是同一件事物的两面。在处理大量数据时,尤其要记得这一点。如果数据缓冲区是按页对齐的,且大小是内建页大小的倍数,那么,对大多数操作系统而言,其处理效率会大幅提升。

在FileChannel上调用map( )方法会创建一个由磁盘文件支持的虚拟内存映射(virtual memory mapping)并在那块虚拟内存空间外部封装一个MappedByteBuffer对象。MappedByteBuffer对象的行为在多数方面类似一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘上的一个文件中。调用get(
)方法会从磁盘文件中获取数据,此数据反映该文件的当前内容,即使在映射建立之后文件已经被一个外部进程做了修改。相似地,对映射的缓冲区实现一个put( )会更新磁盘上的那个文件(假设对该文件您有写的权限),并且您做的修改对于该文件的其他阅读者也是可见的。

与文件锁的范围机制不一样,映射文件的范围不应超过文件的实际大小。如果您请求一个超出文件大小的映射,文件会被增大以匹配映射的大小。假如您给size参数传递的值是Integer.MAX_VALUE,文件大小的值会膨胀到超过2.1GB。即使您请求的是一个只读映射,map( )方法也会尝试这样做并且大多数情况下都会抛出一个IOException异常,因为底层的文件不能被修改。

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

如果映射是以MapMode.READ_ONLY或MAP_MODE.PRIVATE模式建立的,那么调用MappedByteBuffer.force( )方法将不起任何作用,因为永远不会有更改需要应用到磁盘上(但是这样做也是没有害处的)。

由于经常需要从一个位置将文件数据批量传输到另一个位置,即channel 2 channel传输,FileChannel类添加了一些优化方法来提高该传输过程的效率:transferTo(long position, long count , WritableByteChannel target ) 将字节从此通道的文件传输到给定的可写入字节通道,transferFrom(ReadableByteChannel
src, long position , long count )  将字节从给定的可读取字节通道传输到此通道的文件中 。

transferTo( )和transferFrom( )方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。只有FileChannel类有这两个方法,因此channel-to-channel传输中通道之一必须是FileChannel。您不能在socket通道之间直接传输数据,不过socket通道实现WritableByteChannel和ReadableByteChannel接口,因此文件的内容可以用transferTo(
)方法传输给一个socket通道,或者也可以用transferFrom( )方法将数据从一个socket通道直接读取到一个文件中。

直接的通道传输不会更新与某个FileChannel关联的position值。请求的数据传输将从position参数指定的位置开始,传输的字节数不超过count参数的值。实际传输的字节数会由方法返回,可能少于您请求的字节数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: