在Java编程语言中,操作文件IO的时候,通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高,比起bio的模型处理方式,它大大的加大了支持解析读取文件的数量和空间。
正在运行的一个进程,它所需的内存是有可能大于内存条容量之和的,如内存条是256M,程序却要创建一个2G的数据区,那么所有数据不可能都加载到内存(物理内存),必然有数据要放到其他介质中(比如硬盘),待进程需要访问那部分数据时,再调度进入物理内存,而这种场景下,被调度到硬盘的资源空间所占用的存储,我们便将他理解为虚拟内存。
从大体上讲一下MappedByteBuffer 究竟是什么。从继承结构上来讲,MappedByteBuffer 继承自 ByteBuffer,所以,ByteBuffer 有的能力它全有;像变动 position 和 limit 指针啦、包装一个其他种类Buffer的视图啦,内部维护了一个逻辑地址address。
为什么快?因为它使用 direct buffer 的方式读写文件内容,这种方式的学名叫做内存映射。这种方式直接调用系统底层的缓存,没有 JVM 和系统之间的复制操作,所以效率大大的提高了。而且由于它这么快,还可以用它来在进程(或线程)间传递消息,基本上能达到和 “共享内存页” 相同的作用,只不过它是依托实体文件来运行的。
还有就是它可以让读写那些太大而不能放进内存中的文件。实现假定整个文件都放在内存中(实际上,大文件放在内存和虚拟内存中),基本上都可以将它当作一个特别大的数组来访问,这样极大的简化了对于大文件的修改等操作。
FileChannel 提供了 map 方法来把文件映射为 MappedByteBuffer: MappedByteBuffer map(int mode,long position,long size); 可以把文件的从 position 开始的 size 大小的区域映射为 MappedByteBuffer,mode 指出了可访问该内存映像文件的方式,共有三种,分别为:
//一个byte占1B,所以共向文件中存128M的数据
int length = 0x8FFFFFF;
try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"),StandardOpenOption.READ, StandardOpenOption.WRITE);) {MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);for(int i=0;imapBuffer.put((byte)0);}for(int i = length/2;i//像数组一样访问System.out.println(mapBuffer.get(i));}
}
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;public class TestMappedByteBuffer {private static int length = 0x2FFFFFFF;//1Gprivate abstract static class Tester {private String name;public Tester(String name) {this.name = name;}public void runTest() {System.out.print(name + ": ");long start = System.currentTimeMillis();test();System.out.println(System.currentTimeMillis()-start+" ms");}public abstract void test();}private static Tester[] testers = {new Tester("Stream RW") {public void test() {try (FileInputStream fis = new FileInputStream("src/a.txt");DataInputStream dis = new DataInputStream(fis);FileOutputStream fos = new FileOutputStream("src/a.txt");DataOutputStream dos = new DataOutputStream(fos);) {byte b = (byte)0;for(int i=0;idos.writeByte(b);dos.flush();}while (dis.read()!= -1) {}} catch (IOException e) {e.printStackTrace();}}},new Tester("Mapped RW") {public void test() {try (FileChannel channel = FileChannel.open(Paths.get("src/b.txt"),StandardOpenOption.READ, StandardOpenOption.WRITE);) {MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);for(int i=0;imapBuffer.put((byte)0);}mapBuffer.flip();while(mapBuffer.hasRemaining()) {mapBuffer.get();}} catch (IOException e) {e.printStackTrace();}}},new Tester("Mapped PRIVATE") {public void test() {try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"),StandardOpenOption.READ, StandardOpenOption.WRITE);) {MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.PRIVATE, 0, length);for(int i=0;imapBuffer.put((byte)0);}mapBuffer.flip();while(mapBuffer.hasRemaining()) {mapBuffer.get();}} catch (IOException e) {e.printStackTrace();}}}};public static void main(String[] args) {for(Tester tester:testers) {tester.runTest();}}
}
Stream RW->用传统流的方式,最慢,应该是由于用的数据量是 1G,无法全部读入内存,所以它根本无法完成测试。
MapMode.READ_WRITE,它的速度每次差别较大,在 0.6s 和 8s 之间波动,而且很不稳定。
MapMode.PRIVATE就稳得出奇,一直是 1.1s 到 1.2s 之间。
无论是哪个速度都是十分惊人的,但是 MappedByteBuffer 也有不足,就是在数据量很小的时候,表现比较糟糕,那是因为 direct buffer 的初始化时间较长,所以建议大家只有在数据量较大的时候,在用 MappedByteBuffer。
FileChannel提供了map方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。
FileChannel中的几个变量:
接下去通过分析源码,了解一下map过程的内部实现。通过RandomAccessFile获取FileChannel。
public final FileChannel getChannel() {synchronized (this) {if (channel == null) {channel = FileChannelImpl.open(fd, path, true, rw, this);}return channel;}
}
上述实现可以看出,由于synchronized ,只有一个线程能够初始化FileChannel。通过FileChannel.map方法,把文件映射到虚拟内存,并返回逻辑地址address,实现如下:
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {int pagePosition = (int)(position % allocationGranularity);long mapPosition = position - pagePosition;long mapSize = size + pagePosition;try {addr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError x) {System.gc();try {Thread.sleep(100);} catch (InterruptedException y) {Thread.currentThread().interrupt();}try {addr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError y) {// After a second OOME, failthrow new IOException("Map failed", y);}}int isize = (int)size;Unmapper um = new Unmapper(addr, mapSize, isize, mfd);if ((!writable) || (imode == MAP_RO)) {return Util.newMappedByteBufferR(isize,addr + pagePosition,mfd,um);} else {return Util.newMappedByteBuffer(isize,addr + pagePosition,mfd,um);}
}
上述代码可以看出,最终map通过native函数map0完成文件的映射工作。
static MappedByteBuffer newMappedByteBuffer(int size, long addr, FileDescriptor fd, Runnable unmapper) {MappedByteBuffer dbb;if (directByteBufferConstructor == null)initDBBConstructor();dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(new Object[] { new Integer(size),new Long(addr),fd,unmapper }return dbb;
}
// 访问权限
private static void initDBBConstructor() {AccessController.doPrivileged(new PrivilegedAction() {public Void run() {Class> cl = Class.forName("java.nio.DirectByteBuffer");Constructor> ctor = cl.getDeclaredConstructor(new Class>[] { int.class,long.class,FileDescriptor.class,Runnable.class });ctor.setAccessible(true);directByteBufferConstructor = ctor;}});
}
由于FileChannelImpl和DirectByteBuffer不在同一个包中,所以有权限访问问题,通过AccessController类获取DirectByteBuffer的构造器进行实例化。
DirectByteBuffer是MappedByteBuffer的一个子类,其实现了对内存的直接操作。
MappedByteBuffer的get方法最终通过DirectByteBuffer.get方法实现的。
public byte get() {return ((unsafe.getByte(ix(nextGetIndex()))));
}
public byte get(int i) {return ((unsafe.getByte(ix(checkIndex(i)))));
}
private long ix(int i) {return address + (i << 0);
}
map0()函数返回一个地址address,这样就无需调用read或write方法对文件进行读写,通过address就能够操作文件。底层采用unsafe.getByte方法,通过(address + 偏移量)获取指定内存的数据。
第一次访问address所指向的内存区域,导致缺页中断,中断响应函数会在交换区中查找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则从硬盘上将文件指定页读取到物理内存中(非jvm堆内存)。
如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘的虚拟内存中。
从代码层面上看,从硬盘上将文件读入内存,都要经过文件系统进行数据拷贝,并且数据拷贝操作是由文件系统和硬件驱动实现的,理论上来说,拷贝数据的效率是一样的。
采用内存映射的读写效率要比传统的read/write性能高。
通过MappedByteBuffer读取文件
public class MappedByteBufferTest {public static void main(String[] args) {File file = new File("D://data.txt");long len = file.length();byte[] ds = new byte[(int) len];try {MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, len);for (int offset = 0; offset < len; offset++) {byte b = mappedByteBuffer.get();ds[offset] = b;}Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");while (scan.hasNext()) {System.out.print(scan.next() + " ");}} catch (IOException e) {}}
}
MappedByteBuffer使用虚拟内存,因此分配(map)的内存大小不受JVM的-Xmx参数限制,但是也是有大小限制的。
如果当文件超出1.5G限制时,可以通过position参数重新map文件后面的内容。
MappedByteBuffer在处理大文件时的确性能很高,但也存在一些问题,如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。
javadoc中也提到:A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected.*
https://blog.csdn.net/qq_41969879/article/details/81629469
上一篇:C++标准库之:IO库
下一篇:HTML 速查列表