2021-2-19:請問你知道 Java 如何高性能操作文件么?

一般高性能的涉及到存儲框架,例如 RocketMQ,Kafka 這種消息隊列,存儲日誌的時候,都是通過 Java File MMAP 實現的,那麼什麼是 Java File MMAP 呢?

什麼是 Java File MMAP

儘管從JDK 1.4版本開始,Java 內存映射文件(Memory Mapped Files)就已經在java.nio包中,但它對很多程序開發者來說仍然是一個相當新的概念。引入 NIO 後,Java IO 已經相當快,而且內存映射文件提供了 Java 有可能達到的最快 IO 操作,這也是為什麼那些高性能 Java 應用應該使用內存映射文件來持久化數據。
作為 NIO 的一個重要的功能,MMAP 方法為我們提供了將文件的部分或全部映射到內存地址空間的能力,同當這塊內存區域被寫入數據之後會變成臟頁,操作系統會用一定的算法把這些數據寫入到文件中,而我們的 Java 程序不需要去關心這些。這就是內存映射文件的一個關鍵優勢,即使你的程序在剛剛寫入內存後就掛了,操作系統仍然會將內存中的數據寫入文件系統。
另外一個更突出的優勢是共享內存,內存映射文件可以被多個進程同時訪問,起到一種低時延共享內存的作用。

Java File MMAP 與直接操作文件性能對比

package com.github.hashZhang.scanfold.jdk.file;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Random;

public class FileMmapTest {
    public static void main(String[] args) throws Exception {
        //記錄開始時間
        long start = System.currentTimeMillis();
        //通過RandomAccessFile的方式獲取文件的Channel,這種方式針對隨機讀寫的文件較為常用,我們用文件一般是隨機讀寫
        RandomAccessFile randomAccessFile = new RandomAccessFile("./FileMmapTest.txt", "rw");
        FileChannel channel = randomAccessFile.getChannel();
        System.out.println("FileChannel初始化時間:" + (System.currentTimeMillis() - start) + "ms");

        //內存映射文件,模式是READ_WRITE,如果文件不存在,就會被創建
        MappedByteBuffer mappedByteBuffer1 = channel.map(FileChannel.MapMode.READ_WRITE, 0, 128 * 1024 * 1024);
        MappedByteBuffer mappedByteBuffer2 = channel.map(FileChannel.MapMode.READ_WRITE, 0, 128 * 1024 * 1024);

        System.out.println("MMAPFile初始化時間:" + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        testFileChannelSequentialRW(channel);
        System.out.println("FileChannel順序讀寫時間:" + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        testFileMMapSequentialRW(mappedByteBuffer1, mappedByteBuffer2);
        System.out.println("MMAPFile順序讀寫時間:" + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        try {
            testFileChannelRandomRW(channel);
            System.out.println("FileChannel隨機讀寫時間:" + (System.currentTimeMillis() - start) + "ms");
        } finally {
            randomAccessFile.close();
        }

        //文件關閉不影響MMAP寫入和讀取
        start = System.currentTimeMillis();
        testFileMMapRandomRW(mappedByteBuffer1, mappedByteBuffer2);
        System.out.println("MMAPFile隨機讀寫時間:" + (System.currentTimeMillis() - start) + "ms");
    }


    public static void testFileChannelSequentialRW(FileChannel fileChannel) throws Exception {
            byte[] bytes = "測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1".getBytes();
            byte[] to = new byte[bytes.length];
            //分配直接內存,減少複製
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length);
            //順序寫入
            for (int i = 0; i < 100000; i++) {
                byteBuffer.put(bytes);
                byteBuffer.flip();
                fileChannel.write(byteBuffer);
                byteBuffer.flip();
            }

            fileChannel.position(0);
            //順序讀取
            for (int i = 0; i < 100000; i++) {
                fileChannel.read(byteBuffer);
                byteBuffer.flip();
                byteBuffer.get(to);
                byteBuffer.flip();
            }
    }

    public static void testFileMMapSequentialRW(MappedByteBuffer mappedByteBuffer1, MappedByteBuffer mappedByteBuffer2) throws Exception {
        byte[] bytes = "測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2".getBytes();
        byte[] to = new byte[bytes.length];

        //順序寫入
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer1.put(bytes);
        }
        //順序讀取
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer2.get(to);
        }
    }

    public static void testFileChannelRandomRW(FileChannel fileChannel) throws Exception {
        try {
            byte[] bytes = "測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1測試字符串1".getBytes();
            byte[] to = new byte[bytes.length];
            //分配直接內存,減少複製
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length);
            //隨機寫入
            for (int i = 0; i < 100000; i++) {
                byteBuffer.put(bytes);
                byteBuffer.flip();
                fileChannel.position(new Random(i).nextInt(bytes.length*100000));
                fileChannel.write(byteBuffer);
                byteBuffer.flip();
            }
            //隨機讀取
            for (int i = 0; i < 100000; i++) {
                fileChannel.position(new Random(i).nextInt(bytes.length*100000));
                fileChannel.read(byteBuffer);
                byteBuffer.flip();
                byteBuffer.get(to);
                byteBuffer.flip();
            }
        } finally {
            fileChannel.close();
        }
    }

    public static void testFileMMapRandomRW(MappedByteBuffer mappedByteBuffer1, MappedByteBuffer mappedByteBuffer2) throws Exception {
        byte[] bytes = "測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2測試字符串2".getBytes();
        byte[] to = new byte[bytes.length];

        //隨機寫入
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer1.position(new Random(i).nextInt(bytes.length*100000));
            mappedByteBuffer1.put(bytes);
        }
        //隨機讀取
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer2.position(new Random(i).nextInt(bytes.length*100000));
            mappedByteBuffer2.get(to);
        }
    }
}

在這裡,我們初始化了一個文件,並把它映射到了128M的內存中。分FileChannel還有MMAP的方式,通過順序或隨機讀寫,寫了一些內容並讀取一部分內容。

運行結果是:

FileChannel初始化時間:7ms
MMAPFile初始化時間:8ms
FileChannel順序讀寫時間:420ms
MMAPFile順序讀寫時間:20ms
FileChannel隨機讀寫時間:860ms
MMAPFile隨機讀寫時間:45ms

可以看到,通過MMAP內存映射文件的方式操作文件,更加快速,並且性能提升的相當明顯。

微信搜索「我的編程喵」關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

image