一、 java经典文件拷贝方式

1、利用java.io类库,为源文件创建一个FileInputStream读取,然后为目标文件构建一个FileOutputStream,完成写入工作
代码示例如下:

public static void copyFileByStream(File source, File dest)  
	throws IOExecption {
	try (InputStream is = new FileInputStream(source);
		OutputStream os = new FileOutputStream(dest);) 
		{
			byte [] buffer = new byte[1024];
			int length;
			while ((length = is.read(buffer)) > 0) {
				os.write(buffer,0,length);
			} 
	    } 
}

2、利用java.nio类库提供的transferTo或者transferFrom方法实现
代码示例如下:

public static void copyFileByChannel(File source, File dest) 
	throws IOExecption {
	try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
		 FileChannel destChannel = new FileOutputStream(dest).getChannel();) {
		 for (long count = sourceChannel.size;count > 0;) {
		 	long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);
		 	count -= transferred;
		 }
	}
}

以上两种方式进行对比的话,总体来说第二种方式一般比第一种效率更高,速度更快,因为它更能利用现代操作系统底层机制,避免不必要的拷贝和上下文切换。
在比较第一种copy方式和第二种copy方式之前,需要了解两个概念
tips:
1、cpu用户态:普通应用和服务运行在用户态
2、cpu核心态:操作系统内核、硬件驱动运行在核心态
当我们使用输入输出流进行读写时,实际上是进行了多次的上下文的切换,比如读取应用数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核态缓存读取到用户缓存。

而基于nio transferTo的实现方式,在linux和unix上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。同时,transferTo不仅仅是可以用在文件拷贝中,例如读取磁盘文件,然后进行socket发送,同样可以享受这种机制带来的性能和扩展性提高。

二、java io/nio源码结构

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

后两个copy()方法都是基于InputStream/OutputStream实现的,其内部实现是stream在用户态的读写
第一种copy()方法追踪到了UnixCopyFile.c,发现其实它也没有使用transferTo,而是本地技术实现用户态拷贝

如何提高io性能:
1、在程序中,使用缓存等机制,合理减少io次数(在网络通信中,如tcp传输,window大小也可以看作是类似思路)。
2、使用transfeTo等机制,减少上下文切换和额外io操作
3、减少不必要的io过程,比如编解码,序列化与反序列化,比如操作文本文件或者网络通信,如果不是过程中需要使用文本信息,可以考虑不要将二进制转换成字符串,直接传输二进制信息。

nio buffer

buffer有几个属性:
capacity:它反应这个buffer到底有多大,也就是数组长度
position:要操作的数据起始长度
limit:相当于操作的限额。在读取或者写入时,limit的意义很明显是不一样的。比如,读取操作时,很可能将limit设置到所容纳数据的上限,而在写入操作时,则会设置容量或者容量以下的可写限度
mark:记录上一次position的位置,默认是0

buffer的基本操作:
1、创建一个bytebuffer,准备放入数据,capacity当然就是缓冲区大小,而position就是0,limit默认就是capacity的大小。
2、当我们写入几个字节的数据时,position就会跟着水涨船高,但是它不可能超过limit的大小
3、如果我们想把前面写入的数据读出来,就需要调用flip()方法,将limit设置为以前的position的值,然后将position设置为0
4、如果还想从头再读一遍,可以调用rewind,让limit不变,position再次设置为0
direct buffer和垃圾收集
direct buffer: 如果我们看buffer的方法定义,你会发现它定义了isDirect()方法,返回当前buffer是否时direct类型。Java 提供了堆内和堆外内存(direct )buffer,我们可以使用allocate或者allocateDirect方法直接创建
mappedbytebuffer:它将文件按照指定大小直接映射为内存区域,当程序访问这个内存区域时将直接操作这块文件数据,省去了将数据从内核空间向用户空间传输的损耗,可以使用FileChannel.map直接创建mappedbytebuffer,它的本质上也是direct buffer。
java会尽量对direct buffer仅作本地io操作,对于很大的数据量io密集操作,可能会带来很大的性能优势,因为:
1、direct buffer生命周期内内存地址不会再发生更改,进而内核可以安全的对其进行访问,很多io操作都会很高效
2、减少了堆内对象存储的可能额外维护工作,所以访问效率有所提高
但是direct buffer创建和销毁过程中,对会比一般的堆内buffer增加部分开销,所以建议用于长期使用、数据较大的场景。
值得注意的是:大多数垃圾收集过程中,都不会主动收集 Direct Buffer,它的垃圾收集过程,是基于 Cleaner(一个内部实现)和幻象引用(PhantomReference)机制,其本身不是 public 类型,内部实现了一个 Deallocator 负责销毁的逻辑。对它的销毁往往要拖到 full GC 的时候,所以使用不当很容易导致 OutOfMemoryError。
调节direct buffer的参数:
-XX:MaxDirectMemorySize=512M
对于direct buffer的回收建议:
1、在应用程序中,显示的调用System.gc()来强制触发
2、重复使用direct buffer
3、参考netty框架源码的内存管理机制(PlatformDependent),框架会自己在程序中调用释放方法。