实验准备: 需要进行文件复制的文件, 我的文件大小是 1.12 G
实验代码:
@Test
public void test_4() throws Exception{
File file = new File("F:\\temporary\\readFile\\测试文件.rar");
File file2 = new File("F:\\temporary\\writeFile\\测试文件.rar");
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(file2);
Long startTime = System.currentTimeMillis();
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = fis.getChannel();
outChannel = fos.getChannel();
outChannel.transferFrom(inChannel, 0, inChannel.size());
Long endTime = System.currentTimeMillis();
System.out.println("transferFrom 1G 文件耗时: "+(endTime - startTime));
}catch (Exception e) {
e.printStackTrace();
}finally {
inChannel.close();
outChannel.close();
}
}
@Test
public void test_5() throws Exception{
File file = new File("F:\\temporary\\readFile\\测试文件.rar");
File file2 = new File("F:\\temporary\\writeFile\\测试文件.rar");
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(file2);
Long startTime = System.currentTimeMillis();
FileChannel inChannel = null;
FileChannel outChannel = null;
MappedByteBuffer mappedByteBuffer = null;
try {
inChannel = fis.getChannel();
outChannel = fos.getChannel();
mappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
outChannel.write(mappedByteBuffer);
Long endTime = System.currentTimeMillis();
System.out.println("MappedByteBuffer 1G 文件耗时: "+(endTime - startTime));
}catch (Exception e) {
e.printStackTrace();
}finally {
mappedByteBuffer.clear();
inChannel.close();
outChannel.close();
}
}
正如运行结果所展示,我们会发现,对于一个大文件,transferFrom 竟然会比 MappedByteBuffer 这种方式快得多,这里面的原因是什么,为什么会出现这样的结果,于是我在 CSDN 和 开源中国上提出了这个问题,以下是各个大佬们的回答:
其他人的说法都是偏理论性的,而"妹子楼顶有鸽子"有理有据有参考,所以,我便从他所说的地方去着手。
① transfer 底层就是使用的 MappedByteBuffer,这个是真的吗?
② sun.nio.ch.FileChannelImpl.transferFromFileChannel 方法里面是怎么样实现的?
抱着认真严谨的态度以及一丝好奇,开始查看:
① FileInputStream 获取的 channel 对象,源码如下:
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
}
return channel;
}
}
② FileOutputStream获取的 channel 对象,源码如下:
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, false, true, append, this);
}
return channel;
}
}
在这里我们可以发现, 获取的 FileChannel 对象都与 FileChannelImpl 有关,FileInputStream 和 FileOutputStream 获取 FileChannel 的区别暂时看起来只是 FileChannelImpl 传入的参数不同,同时map 和 transferXX 方法都与 FileChannel 有关, 那我们便看看 FileChannelImpl 里面做了些什么吧!
首先便是 FileChannelImpl 对应的两个 open 方法,
// FileInputStream 获取通道时调用的方法
public static FileChannel open(FileDescriptor var0, String var1, boolean var2, boolean var3, Object var4) {
return new FileChannelImpl(var0, var1, var2, var3, false, var4);
}
// FileOutputStream 获取通道时调用的方法
public static FileChannel open(FileDescriptor var0, String var1, boolean var2, boolean var3, boolean var4, Object var5) {
return new FileChannelImpl(var0, var1, var2, var3, var4, var5);
}
由上面可以看到,他们都调用了同一个方法来获取 FileChannel ,不同之处在于所传参数的不同,那这些参数是什么意思呢,这里以 FileOutputStream 的创建调用方法的参数来加以说明。
参数名 | 参数意义 |
---|---|
var0 | 这是一个文件描述符类,用作表示打开文件,开放套接字或其它字节元或信宿的底层机器特定结构的不透明句柄。文件描述符的主要实际用途是创建一个FileInputStream 或 FileOutputStream 来包含它 |
var1 | 路径 path |
var2 | 布尔值,具体作用这里看不出来(源码往下看才知道,这里表示 readable ) |
var3 | 布尔值,具体作用这里看不出来(源码往下看才知道,这里表示 writable ) |
var4 | 为真表示文件为追加打开 |
var5 | 表示一个流对象 |
那接下来再看 FileChannelImpl 的构造器吧!它的源码实现如下:
private FileChannelImpl(FileDescriptor var1, String var2, boolean var3, boolean var4, boolean var5, Object var6) {
this.fd = var1;
this.readable = var3; // 从这里可以看出,这里的参数 var3 var4 表示读写
this.writable = var4;
this.append = var5;
this.parent = var6;
this.path = var2;
this.nd = new FileDispatcherImpl(var5);
}
至此我们获取 FileChannel 完成,接下来先看 FileChannel 的 map 方法吧!
拓展知识点:assert 断言参考
说明 :
assert 是个宏,它的作用是如果它的条件返回错误,则终止程序执行。
在调试结束后,可以通过在包含 #include 的语句之前插入 #define NDEBUG 来禁用 assert 调用,如:
#include
#define NDEBUG
#include
缺点:
频繁的调用会极大的影响程序的性能,增加额外的开销
注意:
① 每个 assert 只验证一个条件,因为同时检验多个条件时,如果断言失败,无法只管的判断是哪个条件失败
② 不能使用改变环境的语句,因为 assert 只在 DEBUG 个生效(底层),如果这么做,会使用程序在正真运行时遇到问题。如:
assert(i++<100); 这是因为如果出错,比如在执行前 i=100,那么这条语句就不会执行,那么i++这条命令就没有执行
③ assert 和后面的语句应空一行,以形成逻辑和视觉上的一致感
④ 有的地方assert 不能代替条件过滤
断言使用的几个原则:
① 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况的区别,后者是必然存在的并且是一定要作出处理的
② 使用断言对函数的参数进行确认
③ 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?” 一旦确定了的假定,就要使用断言对假定进行检查
④ 一般教科书都鼓励程序员们进行防错性的程序涉及,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警
map 方法的源码有点儿长,参数变量也有点儿多,这里先列个表格解释一下它的参数以及意义吧,免得弄混了。
参数 | 参数意义( 这里的参数仅限于这个方法,别的方法没看 ) |
---|---|
var1 | MapMode 类型,表示映射类型 ,有三个,分别是只读(READ_ONLY) 、读写(READ_WRITE) 、私有(PRIVATE) |
var2 | long 类型,映射区域要启动文件的位置,必须为非负 |
var4 | long 类型,要映射的区域的大小,必须是非负数,不得大于 Integer.MAX_VALUE (这里可能会使你有点儿疑惑,特此说明: 虽然这里的参数是 long 类型,但在 FileChannel 接口中,给出的是不得大于 Integer.MAX_VALUE,改方法也做了对应的判断 ) |
var6 | byte类型, 初始值为 -1 ; var1是 READ_ONLY 时,值为 0;是 READ_WRITE 时,值为 1; 是 PRIVATE 时,值为 2 ;用于断言判断 |
var7 | long 类型,初始值为 -1 ; 表示的是逻辑地址 |
var9 | int 类型, 初始值为 -1 ; var1是 READ_ONLY 时,值为 0;是 READ_WRITE 时,值为 1; 是 PRIVATE 时,值为 2 |
var10 | long 类型, 映射大小; |
var12 | int 类型,; |
var13 | Object类型,这个对象是每个 FileChannelImpl 的私有属性对象, 用于锁 ; |
var14 | long 类型,作用暂时未知 |
var16 | long 类型,作用暂时未知 |
var37 | long 类型,映射位置; |
var38 | MappedByteBuffer 类型,初始值为 -1 ; var1是 READ_ONLY 时,值为 0;是 READ_WRITE 时,值为 1; 是 PRIVATE 时,值为 2 |
public MappedByteBuffer map(MapMode var1, long var2, long var4) throws IOException {
this.ensureOpen(); // 确保打开
if (var1 == null) {
throw new NullPointerException("Mode is null");
} else if (var2 < 0L) {
throw new IllegalArgumentException("Negative position");
} else if (var4 < 0L) {
throw new IllegalArgumentException("Negative size");
} else if (var2 + var4 < 0L) {
throw new IllegalArgumentException("Position + size overflow");
} else if (var4 > 2147483647L) {
throw new IllegalArgumentException("Size exceeds Integer.MAX_VALUE");
} else {
byte var6 = -1; // 根据传来的 MapMode 来给 var6 赋值
if (var1 == MapMode.READ_ONLY) {
var6 = 0;
} else if (var1 == MapMode.READ_WRITE) {
var6 = 1;
} else if (var1 == MapMode.PRIVATE) {
var6 = 2;
}
assert var6 >= 0; // 断言,它是一个宏,作用是如果它的条件返回错误,则终止程序执行
// 如果传入的是非只读 并且 当前是 非写入,则抛出异常
if (var1 != MapMode.READ_ONLY && !this.writable) {
throw new NonWritableChannelException();
} else if (!this.readable) {
throw new NonReadableChannelException();
} else {
long var7 = -1L;
int var9 = -1;
MappedByteBuffer var38;
try {
this.begin(); // 开始,这个方法主要是做标记,标记文件打开状态、线程等信息
var9 = this.threads.add(); // 主要对里面的 elts 属性进行了操作
if (!this.isOpen()) {
// 文件没有被打开
Object var34 = null;
return (MappedByteBuffer)var34;
}
Object var13 = this.positionLock; // 获取锁对象
long var10;
int var12;
synchronized(this.positionLock) {
// 加锁
long var14;
do {
// fd 是获取通道是的第一个参数 FileDescriptor,文件描述符类
// this.nd 是获取通道对象的是,在 FileChannelImpl 构造方法中,new 出来的对象FileDispatcherImpl,传入的参数是var5(追加的方式)
// FileDispatcherImpl 的 size 方法,调用了本地方法 size0(FileDescriptor var0)
var14 = this.nd.size(this.fd);
} while(var14 == -3L && this.isOpen()); // 这个地方还不大明白,返回值表示一个大小,但具体代表什么大小?
if (!this.isOpen()) {
var38 = null;
return var38;
}
MappedByteBuffer var17;
if (var14 < var2 + var4) {
// var2代表映射区域启动文件位置 var4表示映射区域大小,var2+var4 则表示最大位置,推测 var14 表示位置信息,那么问题来了,为什么不判断 var2 < var14 呢? 既然不知道,就继续向下看吧
if (!this.writable) {
throw new IOException("Channel not open for writing - cannot extend file to required size");
}
int var16;
do {
// this.fd 表示 FileDescriptor 文件描述符对象,这个对象是创建通道时传来的
// allocate 看字面意思是分配的意思 ,它的方法底层仍是调用的本地方法 truncate0(var1, var2),返回一个 int 类型的值 这个值什么含义 暂时未知 继续向下看吧!
var16 = this.nd.allocate(this.fd, var2 + var4);
} while(var16 == -3 && this.isOpen());
if (!this.isOpen()) {
var17 = null;
return var17;
}
}
if (var4 == 0L) {
// 如果传入映射文件区域大小为 0
var7 = 0L;
FileDescriptor var39 = new FileDescriptor(); // 重新创建了一个文件描述对象
if (this.writable && var6 != 0) {
// 可写 并且 不是只读(0 表示只读,看上面)
// 使用 Util 工具类,创建了一个 MappedByteBuffer 对象
var17 = Util.newMappedByteBuffer(0, 0L, var39, (Runnable)null);
return var17;
}
// 如果上面的if 条件不满足,便执行下面这条语句,仍然是使用 Util 类生成一个 MappedByteBuffer,但注意上面调的方法 和 这个调用的方法不是同一个
var17 = Util.newMappedByteBufferR(0, 0L, var39, (Runnable)null);
return var17;
}
// allocationGranularity 看英文为分配粒度的意思 ,它是在类中的定义是:
// private static final long allocationGranularity;
// 在静态代码块中被初始化,调用的是本地方法 initIDs()
// var2 是映射区域要启动文件的位置
// var4 要映射的区域的大小
var12 = (int)(var2 % allocationGranularity);
long var37 = var2 - (long)var12;
var10 = var4 + (long)var12;
try {
// 这是一个本地方法,看方法名可推测是做映射
// var6 0(READ_ONLY) 1(READ_WRITE) 2(PRIVATE)
// var7 逻辑地址
// var37 映射位置
// var10 映射大小
var7 = this.map0(var6, var37, var10);
} catch (OutOfMemoryError var31) {
System.gc();
try {
// 出现异常,线程休眠0.1秒,并等待gc
Thread.sleep(100L);
} catch (InterruptedException var30) {
Thread.currentThread().interrupt();
}
try {
// 重试一次,如果再失败,则抛出映射失败的异常
var7 = this.map0(var6, var37, var10);
} catch (OutOfMemoryError var29) {
throw new IOException("Map failed", var29);
}
}
}
FileDescriptor var35;
try {
// 重复映射
var35 = this.nd.duplicateForMapping(this.fd);
} catch (IOException var28) {
// 出现异常,放弃映射
unmap0(var7, var10);
throw var28;
}
// 断言检查IO 的状态
assert IOStatus.checkAll(var7);
assert var7 % allocationGranularity == 0L;
//
int var36 = (int)var4;
FileChannelImpl.Unmapper var15 = new FileChannelImpl.Unmapper(var7, var10, var36, var35, null);
if (!this.writable || var6 == 0) {
var38 = Util.newMappedByteBufferR(var36, var7 + (long)var12, var35, var15);
return var38;
}
var38 = Util.newMappedByteBuffer(var36, var7 + (long)var12, var35, var15);
} finally {
this.threads.remove(var9);
this.end(IOStatus.checkAll(var7));
}
return var38;
}
}
}
① Util 工具类的 newMappedByteBuffer 方法
static MappedByteBuffer newMappedByteBuffer(int var0, long var1, FileDescriptor var3, Runnable var4) {
if (directByteBufferConstructor == null) {
initDBBConstructor();
}
try {
MappedByteBuffer var5 = (MappedByteBuffer)directByteBufferConstructor.newInstance(new Integer(var0), new Long(var1), var3, var4);
return var5;
} catch (IllegalAccessException | InvocationTargetException | InstantiationException var7) {
throw new InternalError(var7);
}
}
② Util 工具类的 newMappedByteBufferR方法
static MappedByteBuffer newMappedByteBufferR(int var0, long var1, FileDescriptor var3, Runnable var4) {
if (directByteBufferRConstructor == null) {
initDBBRConstructor();
}
try {
MappedByteBuffer var5 = (MappedByteBuffer)directByteBufferRConstructor.newInstance(new Integer(var0), new Long(var1), var3, var4);
return var5;
} catch (IllegalAccessException | InvocationTargetException | InstantiationException var7) {
throw new InternalError(var7);
}
}
不知道你有没有和我一样,看完了还是感觉很懵,变量太多,而且还不知道它表示的是什么意思,有点儿令人抓狂,但这有什么关系呢,至少对大概有了个了解,认知本来就是一个增量的过程。接下来看看 它的write 方法是如何写入的吧!!!
FileChannel 有两个 write 方法,分别是:
public int write(ByteBuffer var1) throws IOException
public long write(ByteBuffer[] var1, int var2, int var3) throws IOException
先看第一个write 方法吧
public int write(ByteBuffer var1) throws IOException {
this.ensureOpen();
if (!this.writable) {
throw new NonWritableChannelException();
} else {
Object var2 = this.positionLock; // 获取私有锁对象
synchronized(this.positionLock) {
// 加锁
int var3 = 0;
int var4 = -1;
byte var5;
try {
this.begin();
var4 = this.threads.add();
if (this.isOpen()) {
do {
// 使用 IOUtil 工具类,进行写操作
// this.fd 文件描述对象
// var1 ByteBuffer 对象
// -1L 含义未知
// this.nd 文件视图对象
var3 = IOUtil.write(this.fd, var1, -1L, this.nd);
} while(var3 == -3 && this.isOpen());
int var12 = IOStatus.normalize(var3);
return var12;
}
var5 = 0;
} finally {
this.threads.remove(var4);
this.end(var3 > 0);
assert IOStatus.check(var3);
}
return var5;
}
}
}
第二个参数就不看了吧,看 IOUtil 的 write 方法,改方法如下:
static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
if (var1 instanceof DirectBuffer) {
// 如果 var1 是 DirectBuffer 的子类,直接调用了从本地缓冲写方法(writeFromNativeBuffer)
return writeFromNativeBuffer(var0, var1, var2, var4);
} else {
int var5 = var1.position();
int var6 = var1.limit();
assert var5 <= var6;
int var7 = var5 <= var6 ? var6 - var5 : 0;
ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); // 在这里获取临时DirectBuffer
int var10;
try {
var8.put(var1);
var8.flip();
var1.position(var5);
int var9 = writeFromNativeBuffer(var0, var8, var2, var4); // 然后再调用 writeFromNativeBuffer 方法
if (var9 > 0) {
var1.position(var5 + var9);
}
var10 = var9;
} finally {
// 里面会有个判断,判断是否太大(isBufferTooLarge方法),会进行释放操作(free方法)
Util.offerFirstTemporaryDirectBuffer(var8);
}
return var10;
}
}
至此,FileChannel 的 map 方法就大致看完了,接下来看看 transferFrom 方法
public long transferFrom(ReadableByteChannel var1, long var2, long var4) throws IOException {
this.ensureOpen();
if (!var1.isOpen()) {
throw new ClosedChannelException();
} else if (!this.writable) {
throw new NonWritableChannelException();
} else if (var2 >= 0L && var4 >= 0L) {
if (var2 > this.size()) {
return 0L;
} else {
// 在上面的代码中提到过,通道的获取是通过在静态方法中 new FileChannelImpl , 所以此处 我们应该看的是 transferFromFileChannel 方法
return var1 instanceof FileChannelImpl ? this.transferFromFileChannel((FileChannelImpl)var1, var2, var4) : this.transferFromArbitraryChannel(var1, var2, var4);
}
} else {
throw new IllegalArgumentException();
}
}
transferFromFileChannel 方法源码如下:
// var1 是通道对象
// var2 是传输开始的文件中的位置,必须是非负的
// var4 要传输的最大字节数
private long transferFromFileChannel(FileChannelImpl var1, long var2, long var4) throws IOException {
if (!var1.readable) {
throw new NonReadableChannelException();
} else {
Object var6 = var1.positionLock; // 获得锁对象
synchronized(var1.positionLock) {
// 加锁
long var7 = var1.position(); // position 方法返回此通道的文件位置
long var9 = Math.min(var4, var1.size() - var7); //size方法返回的是此通道文件的当前大小
long var11 = var9;
long var13 = var7;
long var15;
while(var11 > 0L) {
var15 = Math.min(var11, 8388608L); // 8*1024*1024 = 8388608L
MappedByteBuffer var17 = var1.map(MapMode.READ_ONLY, var13, var15); // 这里调用了 map 方法
try {
long var18 = (long)this.write(var17, var2); // 然后再调用 write 方法
assert var18 > 0L;
var13 += var18;
var2 += var18;
var11 -= var18;
} catch (IOException var25) {
if (var11 != var9) {
break;
}
throw var25;
} finally {
unmap(var17);
}
}
var15 = var9 - var11;
var1.position(var7 + var15);
return var15;
}
}
}
查看了上面的源码,会发现,transferFrom 方法在底层仍然是调用的是通道的 map 方法来获取到 MappedByteBuffer 对象,这么说的话,它们本质上仍是一样的,那为什么对于同一个大文件的处理所话非的时间会相差这么多呢? 可能你会注意到在 transferFrom 中有这么一段代码,如下:
var15 = Math.min(var11, 8388608L); // 8*1024*1024 = 8388608
MappedByteBuffer var17 = var1.map(MapMode.READ_ONLY, var13, var15);
那么我们可以推测,是不是因为 要映射的区域的大小太大了导致性能差距如此之大的原因,于是启动 Debug,去查看 map 方法映射文件大小在最上面两次实验的值是多少,运行结果如图:
第一次运行,结果如图:
第二次运行,结果如图:
Debug 的结果似乎如猜想的那般,这是由于要映射的区域太大而导致的性能问题。既然如此,就用实验去证明是因为文件映射
实验说明:
文件大小: 1.12G
已知将映射区域设置为文件大小时,耗时为 7961 (数据来源为上面的实验)
实验思路:
① 将 transferFrom 方法内部核心代码复制,
② 重新封装为一个方法
③ 不断修改映射区域大小,查看对应的耗时情况,(因为文件文件1.12G,足够大,修改映射区域比它小就行)
实验代码如下(因为是实验复制的源码,要稍少改一点儿东西,不要在意那些不重要的细节):
public static void main(String[] args) throws Exception {
System.out.println("F:\\temporary\\write.txt");
File file = new File("F:\\temporary\\readFile\\测试文件.rar");
File file2 = new File("F:\\temporary\\writeFile\\测试文件.rar");
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(file2);
Long startTime = System.currentTimeMillis();
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = fis.getChannel();
outChannel = fos.getChannel();
transferFromFileChannel((FileChannelImpl)inChannel, 0, inChannel.size(),(FileChannelImpl)outChannel);
Long endTime = System.currentTimeMillis();
System.out.println("MappedByteBuffer 1G 文件耗时: "+(endTime - startTime));
}catch (Exception e) {
e.printStackTrace();
}finally {
inChannel.close();
outChannel.close();
}
}
public static long transferFromFileChannel(FileChannelImpl var1, long var2, long var4,FileChannelImpl fileChannel) throws IOException {
long var7 = var1.position();
long var9 = Math.min(var4, var1.size() - var7);
long var11 = var9;
long var13 = var7;
long var15;
while(var11 > 0L) {
var15 = Math.min(var11, 2*8388608L);
MappedByteBuffer var17 = var1.map(FileChannel.MapMode.READ_ONLY, var13, var15);
try {
long var18 = fileChannel.write(var17, var2);
assert var18 > 0L;
var13 += var18;
var2 += var18;
var11 -= var18;
} catch (IOException var25) {
if (var11 != var9) {
break;
}
throw var25;
} finally {
unmap(var17);
}
}
var15 = var9 - var11;
var1.position(var7 + var15);
return var15;
}
public static void unmap(MappedByteBuffer var0) {
Cleaner var1 = ((DirectBuffer)var0).cleaner();
if (var1 != null) {
var1.clean();
}
}
为了实验效果,将var15 设置为 8388608 的整数倍, 修改var15 的参数,结果如下:
1213895375(文件大小) / 8388608 = 144.7 ,也就是说整数倍设置小于 140 就行了
var15 参数的值 | 耗时 |
---|---|
8388608L/4 | 1877 |
8388608L/2 | 1991 |
8388608L | 1665 |
2*8388608L | 1853 |
4*8388608L | 4478 |
8*8388608L | 6427 |
10*8388608L | 6937 |
20*8388608L | 7262 |
30*8388608L | 7551 |
50*8388608L | 8064 |
100*8388608L | 7827 |
130*8388608L | 7853 |
实验结果,也确实证明了与映射区域的大小有关
总结:
① 使用 transferFrom 本质上还是在使用 MappedByteBuffer,只是将其进行了封装
② 使用通道的 map 方法时,如果将映射区域设置得过大,仍然会导致性能问题