[轉載] Java直接記憶體與堆記憶體

weixin_34320159發表於2018-05-11

本文轉載自 http://blogxin.cn/2017/01/31/mappedbytebuffer-zerocopy/


JAVA處理大檔案,一般用BufferedReader,BufferedInputStream這類帶緩衝的IO類,不過如果檔案超大的話,更快的方式是採用MappedByteBuffer。

MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, len);

FileChannel提供了map方法來把檔案對映為記憶體映像檔案:可以把檔案的從position開始的size大小的區域對映為記憶體映像檔案,MapMode表示了可訪問該記憶體映像檔案的方式:

  • READ_ONLY,(只讀): 試圖修改得到的緩衝區將導致丟擲ReadOnlyBufferException。(MapMode.READ_ONLY)

  • READ_WRITE(讀/寫): 對得到的緩衝區的更改最終將傳播到檔案;該更改對對映到同一檔案的其他程式不一定是可見的。(MapMode.READ_WRITE)

  • PRIVATE(專用): 對得到的緩衝區的更改不會傳播到檔案,並且該更改對對映到同一檔案的其他程式也不是可見的;相反,會建立緩衝區已修改部分的專用副本。(MapMode.PRIVATE)

MappedByteBuffer是ByteBuffer的子類,其擴充了三個方法:

  • force():緩衝區是READ_WRITE模式下,此方法對緩衝區內容的修改強行寫入檔案;

  • load():將緩衝區的內容載入記憶體,並返回該緩衝區的引用;

  • isLoaded():如果緩衝區的內容在實體記憶體中,則返回真,否則返回假;

對比傳統檔案IO與記憶體對映

在傳統的檔案IO操作中,我們都是呼叫作業系統提供的底層標準IO系統呼叫函式read()、write() ,此時呼叫此函式的程式(在JAVA中即java程式)由當前的使用者態切換到核心態,然後OS的核心程式碼負責將相應的檔案資料讀取到核心的IO緩衝區,然後再把資料從核心IO緩衝區拷貝到程式的私有地址空間中去,這樣便完成了一次IO操作。這麼做是為了減少磁碟的IO操作,為了提高效能而考慮的,因為我們的程式訪問一般都帶有區域性性,也就是所謂的區域性性原理,在這裡主要是指的空間區域性性,即我們訪問了檔案的某一段資料,那麼接下去很可能還會訪問接下去的一段資料,由於磁碟IO操作的速度比直接訪問記憶體慢了好幾個數量級,所以OS根據區域性性原理會在一次read()系統呼叫過程中預讀更多的檔案資料快取在核心IO緩衝區中,當繼續訪問的檔案資料在緩衝區中時便直接拷貝資料到程式私有空間,避免了再次的低效率磁碟IO操作。

記憶體對映檔案是將硬碟上檔案的位置與程式邏輯地址空間中一塊大小相同的區域之間一一對應, 建立記憶體對映由mmap()系統呼叫將檔案直接對映到使用者空間,mmap()中沒有進行資料拷貝,真正的資料拷貝是在缺頁中斷處理時進行的,mmap()會返回一個指標ptr,它指向程式邏輯地址空間中的一個地址,要操作其中的資料時即第一次訪問ptr指向的記憶體區域,必須通過MMU將邏輯地址轉換成實體地址,MMU在地址對映表中是無法找到與ptr相對應的實體地址的,也就是MMU失敗,將產生一個缺頁中斷,缺頁中斷的中斷響應函式會通過mmap()建立的對映關係,從硬碟上將檔案讀取到實體記憶體中,這個過程只進行了一次資料拷貝。因此,記憶體對映的效率要比read/write呼叫效率高。

通過下面的例子測試普通檔案通道IO和記憶體對映IO的速度:

public class FileChannelTest {
    public static void main(String[] args) throws IOException {
        testFileChannel();
        testMappedByteBuffer();
    }
    public static void testFileChannel() throws IOException {
        RandomAccessFile file = null;
        try {
            file = new RandomAccessFile("/Users/xin/Downloads/b.txt", "rw");
            FileChannel channel = file.getChannel();
            ByteBuffer buff = ByteBuffer.allocate(1024);
            long timeBegin = System.currentTimeMillis();
            while (channel.read(buff) != -1) {
                buff.flip();
                buff.clear();
            }
            long timeEnd = System.currentTimeMillis();
            System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (file != null) {
                    file.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void testMappedByteBuffer() throws IOException {
        RandomAccessFile file = null;
        try {
            file = new RandomAccessFile("/Users/xin/Downloads/b.txt", "rw");
            FileChannel fc = file.getChannel();
            int len = (int) file.length();
            MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, len);
            byte[] b = new byte[1024];
            long timeBegin = System.currentTimeMillis();
            for (int offset = 0; offset < len; offset += 1024) {
                if (len - offset > 1024) {
                    buffer.get(b);
                } else {
                    buffer.get(new byte[len - offset]);
                }
            }
            long timeEnd = System.currentTimeMillis();
            System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (file != null) {
                    file.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

控制檯輸出結果如下:

Read time: 302ms

Read time: 61ms

根據測試結果證明了記憶體對映檔案比檔案通道速度快很多。

記憶體對映檔案和之前說的標準IO操作最大的不同之處就在於它雖然最終也是要從磁碟讀取資料,但是它並不需要將資料讀取到OS核心緩衝區,而是直接將程式的使用者私有地址空間中的一部分割槽域與檔案物件建立起對映關係,就好像直接從記憶體中讀、寫檔案一樣,速度當然快了。記憶體對映檔案的效率比標準IO高的重要原因就是因為少了把資料拷貝到OS核心緩衝區這一步。

zerocopy技術

zerocopy技術的目標就是提高IO密集型JAVA應用程式的效能。IO操作需要資料頻繁地在核心緩衝區和使用者緩衝區之間拷貝,而zerocopy技術可以減少這種拷貝的次數,同時也降低了上下文切換(使用者態與核心態之間的切換)的次數。在Java中的應用就是java.nio.channels.FileChannel類的transferTo()方法可以直接將位元組傳送到可寫的通道中,並不需要將位元組轉入使用者緩衝區。

package java.nio.channels;
public abstract class FileChannel
    extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
    ... ...
    
    public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
        
    ... ...
}
FileChannel fileChannel = fis.getChannel();
fileChannel.transferTo(0, fileChannel.size(), targetChannel);

正常讀取檔案再傳送出去需要經歷一下幾個步驟:

從本地磁碟或者網路讀取資料--->資料進入核心緩衝區--->使用者緩衝區--->核心緩衝區--->通過socket傳送

資料每次在核心緩衝區與使用者緩衝區之間的拷貝會消耗CPU以及記憶體的頻寬。而zerocopy有效減少了這種拷貝次數,使用者程式執行transferTo()方法,導致一次系統呼叫,從使用者態切換到核心態,完成的動作是:

從本地磁碟或者網路讀取資料--->資料進入核心緩衝區--->通過socket傳送

zerocopy好處就是減少了將資料從核心緩衝區拷貝到使用者緩衝區,再拷貝回核心緩衝區,減少了拷貝次數和上下文切換次數。

相關文章