java的文件拷贝方式及NIO相关知识扩展:
java的文件拷贝方式:
-
利于FileInputStream读取,利用FileOutputStream写入;代码如下:(此处buffer与Buffered stream效果一致,取其一即可,无需两者同时使用,建议使用buffered stream)
public static void copy(String source,String target) throws IOException { try(FileInputStream input = new FileInputStream(source); FileOutputStream out = new FileOutputStream(target)){ BufferedOutputStream bufferedOut = new BufferedOutputStream(out); BufferedInputStream bufferedInput = new BufferedInputStream(input); byte[] buffer = new byte[10240]; int size = 0; while((size = bufferedInput.read(buffer)) > 0) { bufferedOut.write(buffer,0,size); } bufferedOut.flush(); } }
-
利用java.nio提供的trasferTo或者trasferFrom方法实现,它更能利用现代操作系统的底层机制,避免不必要的拷贝和上下文切换,代码如下:
public static void copy2(String source,String target) throws IOException { try(FileChannel input = new FileInputStream(source).getChannel(); FileChannel out = new FileOutputStream(target).getChannel();){ long length = input.size(); //while( length > 0) { //length -= input.transferTo(input.position(),length,out); //} //or while(length > 0) { //transferFrom并不会改变out流的position位置, //但是会改变的input流的position位置; length -= out.transferFrom(input,input.position(),length); } } }
为什么transferTo或者transferFrom有性能优势:
-
理解内核态空间和用户态空间:
- 普通应用程序运行于用户态空间,权限受限,无法对底层IO进行操作,相关的操作需要切换到内核态空间才能执行;这就会带来空间切换的开销;系统内核、硬件驱动都运行于内核态空间,具有较高的权限;
- 用户态空间下进行文件拷贝示意图:
- 用户态空间下文件拷贝实际上进行了多次上下文切换,且数据需要先从磁盘读取到内核缓存,在切换到用户态将数据从内核缓存读取到用户缓存;写入操作也是类似,这就带来了额外开销,降低IO效率;
-
transferTo或者transferFrom则能利用底层操作系统机制,在Linux上使用零拷贝技术,数据传输无需用户态参与,省去了上下文切换的开销与不必要的内存拷贝;示意图如下:
标准库(JDK1.8u201)提供的相关拷贝(java.nio.file.Files.copy):
public static Path copy(Path source, Path target, CopyOption... options)throws IOException
public static long copy(InputStream in, Path target, CopyOption... options) throws IOException
public static long copy(Path source, OutputStream out) throws IOException {
以上方法均使用的是用户态空间拷贝;其中第一个方法是使用本地技术实现的用户态拷贝;1.8以后的JDK版本下面两个函数版本可能会采用transferTo或者transferFrom进行优化;
提高文件拷贝性能的原则:
- 尽量使用缓存,合理减少IO访问次数;
- 使用transferTo机制,减少上下文切换和不必要内存拷贝;
- 减少不必要的转换,直接传输二进制;如:编解码、序列化和反序列化;
掌握NIO Buffer;
1. buffer属性:
- capacity,它反映这个buffer的大小;
- limit:它反映这个buffer的可读或者可写限度;
- position:该buffer目前读取、写入的位置;都是从0开始的;
- mark:便利性的考虑,可以通过mark()记录地址,reset将position设置为上一次mark的位置;clear,flip,rewind都会清零mark(设置为-1);
2. buffer注意:
- 即使调用了flip方法,也需要注意的是该buffer依然能够写入数据,并且position会随着写入字节水涨船高,如果超过limit大小,则会抛出BufferOverflowException;
- buffer.get()方法可以传入一个byte数组,注意,该方法会根据byte数组的大小来决定读取多少,使用get方法从0开始填充到数组的最后一位,如果byte大小超过了ByteBuffer的可以读取的限度,则会抛出BufferUnderflowException;
3. buffer的方法:
- put(),get():基本的写入和读取方法,会影响position的数值,还有putInt,putFloat等版本,int占4个byte空间,如果是ByteBuffer则position会增长4;
- flip方法:将position设置为0,limit设置为当前的position那里,并清除mark;
- rewind:position设置为0,请出mark;
- clear:position设置为0,limit设置为capacity,清除mark;
- hasRemaining():返回当前position和limit之间是否还有元素;
- Remaining():返回还有多少元素;
- reset():position设置为mark的位置;
4. direct buffer、MappedByteBuffer和垃圾收集
MappedByteBuffer:FileChannel.map()可以创建,本质上也是一种DirectBuffer,它将文件按照指定大小直接映射为内存区域,当程序访问内存区域时,将直接操作文件数据,省去了数据从内核空间向用户空间传输的损耗,使用写模式时注意使用RandomAccessFile与其配合,因为无论是FileInputStream还是FileOutputStream均不能满足其map中的读写模式,但又没有提供只写模式,但其也存在一些问题:如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的;
获取direct buffer:ByteBuffer.allocateDirect(1000)来获取DirectBuffer;
检测是否是DirectBuffer:buffer提供了isDirect函数来检测是否是DirectBuffer
-
Direct Buffer 在实际使用过程中,java会尽量对其仅作本地IO操作,对于很多大数据量的IO操作,会带来非常大的性能优势:
- 因为生命周期内地址都不会再改变,内核可以安全对其进行访问,所以很多IO操作都会很高效;
- 因为是堆外对象,省去了额外的维护开销,访问效率更高;
-
Direct Buffer使用注意:
创建和销毁都比一般buffer开销要大;
-
-Xmx不能影响其使用,因为其是堆外存储;限制方式如下:
-XX:MaxDirectMemorySize=512M所以在使用DirectBuffer时,应注意内存大小需要考虑其空间占用;否则容易导致OOM;
大部分gc不会主动收集DirectBuffer;它的销毁往往要等到FullGC;
-
使用建议:
- 重复使用DirectBuffer;
- 跟踪和诊断DirectBuffer内存占用:
-
-XX:NativeMemoryTracking={summary|detail}可以开启Native Memory Tracking(NMT)特性,激活 NMT 通常都会导致 JVM 出现 5%~10% 的性能下降,采用以下命令进行交互式对比:
// 打印NMT信息 jcmd <pid> VM.native_memory detail // 进行baseline,以对比分配内存变化 jcmd <pid> VM.native_memory baseline // 进行baseline,以对比分配内存变化 jcmd <pid> VM.native_memory detail.diff
-
Scattering Reads And Gathering Writes
定义:
Scattering Reads
Scattering Reads是指数据从一个channel读取到多个buffer中。如下图描述:
Gathering Writes
Gathering Writes是指数据从多个buffer写入到同一个channel。如下图描述:
使用语法:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] dataBuffers = {header, body};
//注意只会将buffer中的有效数据写入或者读取channel,也就是说受limit的影响,
//如果channel中的数据大于buffer总空间,不会抛出异常,依次读取写入buffer,能读多少读多少;
channle.write(dataBuffers);
channel.read(resultBuffers);