问题
最近遇到一个场景:
程序运行过程中有多个节点对象,其中有三个节点的状态需要持久化,其余节点的状态不需要持久化,所有的节点在运行过程中的状态都会不断变化,程序运行过程中需要保证三个需要持久化节点的数据在大部分场景下崩溃后,下次重启可以读入上次程序崩溃前的状态,此外每个节点有个单独的ID。
由于是单机程序,所以使用了一个Map来存储这些数据
对于单机程序,为了简单,自然想到使用文件来进行数据的持久化,由于有现成的XML工具包使用,所以刚开始时选择了使用XML文件来持久化数据,也就是每次数据变化后,将需要持久化的三个节点的数据一同写到文件中。这样实现的方式就是简单,但是问题来了,在写文件的过程中,由于使用XML格式,所以先要将Map中的数据取出,然后new出XML格式数据的节点,最后写入到文件中。这个过程没什么问题,但是效率跟不上,当程序中需要持久化的三个节点改变状态的次数较多时,每次都将这些数据写到文件中影响了整个程序的运行进度。
看见写文件这么慢,首先想到的自然是去掉xml,直接写将节点数据写入到文件中,既然这样的话,那么三个节点有三个不同的ID,写入同一个文件就需要分成三段,如果每次都将三种数据一同写入到文件中,那么就使用一个分隔符,但是在程序运行过程中,每次只改变一个节点的数据,就要将三个节点的数据写入文件,这样显然比较浪费。所以再去掉xml格式之后,再将这三个节点的数据分别写到三个不同的文件中,并且以节点ID作为文件名用以区分三个不同的节点。接下来就要找一种更快将数据写入文件的方法了。
Java中到底哪个类是写数据是最快的呢?看到有人说MappedByteBuffer读写文件都比较快,所以就想使用MappedByteBuffer试试,于是写了一段测试代码,顺便熟悉MappedByteBuffer的接口。同时也将MappedByteBuffer的性能与FileOutputStream对比,代码如下:
public static void main(String[] args) throws FileNotFoundException, IOException { StringBuilder sb = new StringBuilder(); for(int i = 0; i < 1000; i++) { sb.append('a'); } String s = sb.toString(); long astart = System.currentTimeMillis(); RandomAccessFile file = new RandomAccessFile("D:\\ran", "rwd"); file.write(s.getBytes()); file.close(); long start = System.currentTimeMillis(); RandomAccessFile raf = new RandomAccessFile("D:\\ran_map", "rwd"); MappedByteBuffer mbb = raf.getChannel().map(MapMode.READ_WRITE, 0, s.getBytes().length); mbb.put(s.getBytes()); raf.close(); long end = System.currentTimeMillis(); OutputStream out = new FileOutputStream("D:\\filout"); out.write(s.getBytes()); out.close(); long endd = System.currentTimeMillis(); System.out.println((start-astart) + ":" + (end-start) + ":" + (endd-end));
输出结果为:
五次执行结果为:
34:10:0 13:21:0 29:16:1 32:17:0 21:17:0
没想到,FileOutputStream写文件竟然比MappedByteBuffer和RandomAccessFile快这么多。照这样看,网上的那些说法都不科学呀。
进入到FileOutputStream的write方法:
public void write(byte b[]) throws IOException { writeBytes(b, 0, b.length); } private native void writeBytes(byte b[], int off, int len) throws IOException;
FileOutputStream通过write(byte[] b)方法直接调用了native方法writeBytes(byte b[], int off, int len)将数据写入到文件中。
而MappedByteBuffer.put(byte[] src)方法依次调用了:
public final ByteBuffer put(byte[] src) { return put(src, 0, src.length); } public ByteBuffer put(byte[] src, int offset, int length) { if ((length << 0) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) { checkBounds(offset, length, src.length); int pos = position(); int lim = limit(); assert (pos <= lim); int rem = (pos <= lim ? lim - pos : 0); if (length > rem) throw new BufferOverflowException(); Bits.copyFromArray(src, arrayBaseOffset, offset << 0,ix(pos), length << 0); position(pos + length); } else { super.put(src, offset, length); } return this; }
这两个方法调用都是通过DirectByteBuffer类的对象调用的,DirectByteBuffer是直接内存的方式,也就是直接使用内存,而没有通过JVM的堆,方法中先检查是否越界,然后获取到当前的position,再将数据进行拷贝到直接内存。
为什么会比较耗时呢?这段直接内存的创建和销毁通常比JVM对耗时一些,它不熟垃圾手机的控制,因为它在JVM外部。
JDK中的FileChannel.map()方法的注释最后一段是这样的:
For most operating systems, mapping a file into memory is more expensive than reading or writing a few tens of kilobytes of data via the usual
read
and write
methods. From the standpoint of performance it is generally only worth mapping relatively large files into memory.
MappedByteBuffer类将一块内存与文件的某个部分进行映射,然后你就可以通过这个MappedByteBuffer对该文件的这部分进行读写操作了。这样不用加载整个文件,也可以进行读写。所以MappedByteBuffer使用场景是针对大文件,并且有连续的读写操作的场景。
可是对于本文刚开始的那个场景,单纯的写文件,FileOutputStream不比MappedByteBuffer慢。缓冲写的本质是使用一块内存区来保存将要写出到磁盘的数据,然后等待缓冲区满后将缓冲区中的数据一起写到文件中,假设缓冲区大小为n个字节,那么相对于对于每个字节都调用一次IO,使用缓冲区时就对n个字节调用一次IO,所以这样就提升了写的效率了。而对于文章中的这个场景,因为是将一个字节数组的数据一次性写入到文件中,所以就算是调用FIleOutputStream.write(byte[] b)方法,也是通过一次调用native的write方法写入文件的,在此种场景下无需使用缓冲。
对于测试代码中RandomAccessFile写数据时调用的方法为:
public void write(byte b[]) throws IOException { writeBytes(b, 0, b.length); } private native void writeBytes(byte b[], int off, int len) throws IOException;
RandomAccessFile.writeBytes(byte b[], int off, int len)
方法与FileOutputStream.writeBytes(byte b[], int off, int len)
方法都是调用native方法写文件,为什么效率就不一样呢?难道RandomAccessFile写文件时还有额外的操作?留个坑,以后再来看。
Reference
花1K内存实现高效I/O的RandomAccessFile类
java之HeapByteBuffer&DirectByteBuffer以及回收DirectByteBuffer