【Java基础知识 18】通过FileUtils.copyFile探索IO原理

一、FileUtils.copyFile

1、从实例出发

一般开发的时候,都是通过文件工具类进行文件的copy,那么它的性能怎么样呢?它是怎么实现的呢?今天就来分析以下FileUtils.copyFile。

private static void copyFileByUtils() {
    
    
        String srcFilePath = "H:\\CSDN\\JWFS.rar";// 文件大小 68.8 MB
        String destFilePath = "H:\\CSDN\\netty\\nio\\JWFS.rar";
        long start = System.currentTimeMillis();

        try {
    
    
            FileUtils.copyFile(new File(srcFilePath),new File(destFilePath));
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("copyFileByUtils 消耗:"+(end-start)+"毫秒");
    }

在这里插入图片描述

2、还是蛮快的,探索源码一番…

private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
    
    
        if (destFile.exists() && destFile.isDirectory()) {
    
    
            throw new IOException("Destination '" + destFile + "' exists but is a directory");
        } else {
    
    
            FileInputStream fis = null;
            FileOutputStream fos = null;
            FileChannel input = null;
            FileChannel output = null;

            try {
    
    
                fis = new FileInputStream(srcFile);
                fos = new FileOutputStream(destFile);
                input = fis.getChannel();
                output = fos.getChannel();
                long size = input.size();
                long pos = 0L;

                for(long count = 0L; pos < size; pos += output.transferFrom(input, pos, count)) {
    
    
                    count = size - pos > 31457280L ? 31457280L : size - pos;
                }
            } finally {
    
    
                IOUtils.closeQuietly(output);
                IOUtils.closeQuietly(fos);
                IOUtils.closeQuietly(input);
                IOUtils.closeQuietly(fis);
            }

            if (srcFile.length() != destFile.length()) {
    
    
                throw new IOException("Failed to copy full contents from '" + srcFile + "' to '" + destFile + "'");
            } else {
    
    
                if (preserveFileDate) {
    
    
                    destFile.setLastModified(srcFile.lastModified());
                }

            }
        }
    }

发现了一个生僻的词汇,FileChannel,研究一下。

二、FileChannel

在这里插入图片描述
这种方式是“在非直接缓冲区中,通过Channel实现文件的复制”

1、读操作

  1. 将磁盘文件读取到操作系统OS提供的内核地址空间的内存中,第一次复制,OS上下文切换到内核模式;
  2. 将内核地址空间内存中的文件内容复制到JVM提供的用户地址空间的内存中,第二次复制,OS上下文切换到用户模式;

2、写操作

  1. 将用户地址空间的JVM内存中的文件内容复制到OS提供的内核地址空间中的内存中,第一次复制,OS上下文切换到内核模式;
  2. 将内核地址空间中内存的文件内容写入磁盘文件,第二次复制,写入操作完毕后,OS上下文最终切换到用户模式;

JVM控制的内存称为堆内内存,一般用Java操作的内存都属于堆内内存,堆内内存由JVM统一管理,根据上面的流程图可以发现,一次文件的读写要经过4次copy和4次用户控件与内核空间的上下文切换。

3、代码实例

package com.guor.demo.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class CopyFileTest {
    
    

    private static void copyFileByChannel() {
    
    
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        FileChannel intChannel = null;
        FileChannel outChannel = null;

		String srcFilePath = "H:\\CSDN\\JWFS.rar";// 文件大小 68.8 MB
        String destFilePath = "H:\\CSDN\\netty\\nio\\JWFS.rar";
        long start = System.currentTimeMillis();

        try {
    
    
            fileInputStream = new FileInputStream(srcFilePath);
            fileOutputStream = new FileOutputStream(destFilePath);

            // 获取通道
            intChannel = fileInputStream.getChannel();
            outChannel = fileOutputStream.getChannel();

            // 创建非直接缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (intChannel.read(buffer)!=-1){
    
    
                buffer.flip();
                outChannel.write(buffer);
                buffer.clear();
            }
            long end = System.currentTimeMillis();
            System.out.println("copyFileByChannel 消耗:"+(end-start)+"毫秒");
        }catch (Exception e){
    
    
            System.out.println(e);
        }finally {
    
    
            if(outChannel!=null){
    
    
                try {
    
    
                    outChannel.close();
                } catch (IOException e) {
    
    
                }
            }
            if(intChannel!=null){
    
    
                try {
    
    
                    intChannel.close();
                } catch (IOException e) {
    
    
                }
            }
            if(fileOutputStream!=null){
    
    
                try {
    
    
                    fileOutputStream.close();
                } catch (IOException e) {
    
    
                }
            }
            if(fileInputStream!=null){
    
    
                try {
    
    
                    fileInputStream.close();
                } catch (IOException e) {
    
    
                }
            }
        }
    }

    public static void main(String[] args) {
    
    
        copyFileByChannel();
    }
}

4、控制台输出

在这里插入图片描述
感觉和FileUtils.copyFile的速度还是有差距的。

三、如何减少copy和上下文切换的次数?

1、为什么不能舍弃内核空间这一步,直接读取到用户空间呢?

因为JVM中有GC,GC会不定期的清理没用的对象,并且压缩文件区域,如果某一时刻正在JVM中复制一个文件,但由于GC的压缩操作可能会引起文件在JVM中的位置发生改变,进而导致程序出现异常。因此,为了保证文件在内存中的位置不发生改变,只能将其放入OS的内存中。

2、如何减少copy和上下文切换的次数?

使用直接缓冲区,就可以在JVM中通过一个address变量指向OS中的一块内存(称为物理映射文件),之后,就可以通过JVM直接使用OS中的内存。

下面介绍一个新的方式“在直接缓冲区中,通过Channel实现文件的复制”
在这里插入图片描述
这样,数据的赋值操作都是在内核空间里进行的,用户空间与内核空间直接的复制次数为0,也就是零拷贝。

扫描二维码关注公众号,回复: 14547664 查看本文章

3、代码实例

private static void copyFileByMapped() {
    
    

        String srcFilePath = "H:\\CSDN\\JWFS.rar";// 文件大小 68.8 MB
        String destFilePath = "H:\\CSDN\\netty\\nio\\JWFS.rar";
        long start = System.currentTimeMillis();
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
    
    
            // 文件的输入通道
            inChannel = FileChannel.open(Paths.get(srcFilePath), StandardOpenOption.READ);
            // 文件的输出通道
            outChannel = FileChannel.open(Paths.get(destFilePath), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);

            // 输入通道和输出通道之间的内存映射文件,内存映射文件处于堆外内存
            MappedByteBuffer inMappedBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
            MappedByteBuffer outMappedBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());

            // 直接对内存映射文件进行读写
            byte[] bytes = new byte[inMappedBuffer.limit()];
            inMappedBuffer.get(bytes);
            outMappedBuffer.put(bytes);

            long end = System.currentTimeMillis();
            System.out.println("copyFileByMapped 消耗:"+(end-start)+"毫秒");
        }catch (Exception e){
    
    
            System.out.println(e);
        }finally {
    
    
            try {
    
    
                inChannel.close();
                outChannel.close();
            } catch (IOException e) {
    
    
            }
        }
    }

4、控制台输出

在这里插入图片描述
直接缓冲区拷贝文件比非直接缓冲区拷贝文件,快了整整10倍。

哪吒精品系列文章

Java学习路线总结,搬砖工逆袭Java架构师

10万字208道Java经典面试题总结(附答案)

SQL性能优化的21个小技巧

Java基础教程系列

Spring Boot 进阶实战
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/guorui_java/article/details/127152084