我们知道,在linux系统中实现文件零拷贝的方式有两种:mmap和sendfile,对于这两个api实现零拷贝的区别就不做过多的赘述,网上有很多这方面的分析文章,在使用中我大致总结如下:

1、实现零拷贝的文件大小不能超过2G
2、mmap方式可以对映射文件进行编辑,sendfile只能对文件做拷贝不能编辑

在java中这两个技术对应的api分别是MappedByteBuffer类和transferTo()方法,下面就这两个api使用做一下总结:

MappedByteBuffer类使用

MappedByteBuffer类通常不是通过new出来的,它需要通过FileChannel的map方法获取到,代码如下:

public class Test {

    public static void main(String[] args) throws IOException {
        File file = new File("E:\\test.txt");
        file.createNewFile();

        FileChannel fileChannel = FileChannel.open(Paths.get(file.getPath()), StandardOpenOption.WRITE, StandardOpenOption.READ);
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
        fileChannel.close();
        mappedByteBuffer.putLong(1024L);
        mappedByteBuffer.force();

        mappedByteBuffer.flip();
        System.out.println(mappedByteBuffer.getLong());
    }
}

我们看到当调用了FileChannel的map方法后就调用fileChannel的close()方法关闭通道,但这样并不会影响磁盘文件和内存地址的映射关系,以后所有对内存的修改都会被写入到磁盘文件中;但这种写入不会立即执行,所有的修改会被写入到操作系统的PageCache中,如果想将数据刷入磁盘文件里,需要我们调用force()方法通知刷新数据。
FileChannel的map方法有三个参数:

参数名

解释

MapMode

映射模式,可取值有READ_ONLY(只读映射)、READ_WRITE(读写映射)、PRIVATE(私有映射),READ_ONLY只支持读,READ_WRITE支持读写,而PRIVATE只支持在内存中修改,不会写回磁盘

position

映射文件的开始位置

size

映射文件的大小

position 和 size 构成了文件的映射区域,可以是整个文件,也可以是文件的某一部分,单位为字节。

也通过RandomAccessFile获取FileChannel,使用方法如下:

public class Test {

    public static void main(String[] args) throws IOException {
        File file = new File("E:\\test.txt");
        FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
        fileChannel.close();
        mappedByteBuffer.putLong(1024L);
        mappedByteBuffer.force();

        mappedByteBuffer.flip();
        System.out.println(mappedByteBuffer.getLong());
    }
}

MappedByteBuffer 方法进行了文件和内存映射关系后,会加快文件的读取和写入操作,尤其是顺序读写会有明显的性能提醒。

MappedByteBuffer跟其他ByteBuffer类一样,要进行读写区分,里面包含的常用属性和api如下:

常见属性:

属性名

解释

position

当前的下标位置,表示进行下一个读或写操作的起始位置

limit

结束标记下标位置,表示进行下一个读或写操作的最大位置

capacity

当前操作的ByteBuffer容量

mark

自定义的标记位置

常用方法:

方法名

解释

compact()

将ByteBuffer切换到写模式,position会移动到缓冲区开始位置,position会移动到缓冲区最大位置

flip()

将ByteBuffer切换到读模式,limit移动到position位置,position会移动到缓冲区开始位置

clear()

清空ByteBuffer,position移动到开始位置,limit移动到最大位置,清除mark值

mark()

标记当前position位置,配合reset()使用回到之前的position位置

reset()

回到mark()标记时的position位置


transferTo()

transferTo()方法是sendfile方式零拷贝在java中的实现,但是这个方法只能针对文件的发送和接收、转移文件位置等操作,它不能对文件内容进行修改操作,所以这个api更多的是使用在网络文件的上传和下载,文件位置转移时提升性能,下面介绍两种场景下的使用

1、文件上传

进行文件上传时,SpringBoot中的类 MultipartFile 已经提供了transferTo()的方法,我们接收文件时可以使用如下方法将接收到的文件保存到本地磁盘中:

@PostMapping("/upload/file")
    public Result uploadFile(@RequestPart(value = "file") MultipartFile file) throws Exception {
        String filePath = "E:\\" + file.getOriginalFilename();
        file.transferTo(new File(filePath));
        return Result.success();
    }
2、文件下载

当进行文件下载时,通过response获取到的输出流不支持transferTo()方法,这时我们需要通过Channels提供的工具类进行转换,使用方式如下:

@GetMapping("/download/file")
    public void downloadFile(HttpServletResponse response) throws IOException {
        File file = new File("E:\\jdk-8u161-linux-i586.tar.gz");
        try (FileInputStream fis = new FileInputStream(file);
             ServletOutputStream out = response.getOutputStream();
             WritableByteChannel channel = Channels.newChannel(out);) {

            String downFileName = URLEncoder.encode(file.getName(), "UTF-8");
            response.setHeader("Content-Disposition", "attachment; filename=" + downFileName);
            response.setContentType("application/octet-stream");

            fis.getChannel().transferTo(0, fis.available(), channel);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
3、文件转移

当我们将一个文件从一个磁盘拷贝到另外一个磁盘时,也可以使用transferTo来处理:

public class Test {

    public static void main(String[] args) {
        File srcFile = new File("E:\\jdk-8u161-linux-i586.tar.gz");
        File destFile = new File("D:\\jdk-8u161-linux-i586.tar.gz");

        try (FileChannel srcChannel = new FileInputStream(srcFile).getChannel();
             FileChannel destChannel = new FileOutputStream(destFile).getChannel();) {
            srcChannel.transferTo(0, srcFile.length(), destChannel);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}