java nio—buffer的簡單介紹以及堆外內存的分析

作用

NIO提供了一系列buffer類,用作緩存。可以直接從channel中讀數據到buffer,也可以從buffer中寫數據到channel。緩衝區本質上是一塊固定大小的內存,其作用是一個存儲器或運輸器。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。

類圖

在這裏插入圖片描述

Buffer的四個屬性

  • 容量(capacity):緩衝區能夠容納的數據元素的最大數量。這一容量在緩衝區創建時被設定,並且永遠不能被改變
  • 上界(limit):緩衝區的第一個不能被讀或寫的元素。或者說,緩衝區中現存元素的計數
  • 位置(position):下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新
  • 標記(mark):下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新一個備忘位置。調用 mark( )來設定 mark = postion。調用 reset( )設定 position =mark。標記在設定前是未定義的(undefined)。這四個屬性之間總是遵循以下關係:0 <= mark <= position <= limit <= capacity

Buffer的基本用法

使用Buffer讀寫數據一般遵循以下四個步驟:

  1. 寫入數據到Buffer
  2. 調用flip()方法
  3. 從Buffer中讀取數據
  4. 調用clear()方法或者compact()方法

當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有數據。

一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用clear()或compact()方法。clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。
舉個栗子:
定義一個容量是10的buffer,填入hello:
在這裏插入圖片描述
這時候,limit代表,最多可以寫多少,position代表,即將寫入的位置
然後進行flip:
在這裏插入圖片描述
flip之後,limit代表最多可以讀多少,position,代表開始讀的位置。

三種類型buffer

java nio提供了三種不同的buffer,HeapByteBuffer、DirectByteBuffer、MappedByteBuffer。
一些不同:

  • HeapByteBuffer是在jvm堆上申請的內存,而DirectByteBuffer、MappedByteBuffer是在堆外申請的內存。
  • MappedByteBuffer藉助了mmap(內存映射文件),提高了文件讀取效率

transferTo:適用於應用程序無需對文件數據進行任何操作的場景;
map:適用於應用程序需要操作文件數據的場景;

HeapByteBuffer

在java堆上申請的內存。通過ByteBuffer.allocate方法申請jvm堆上內存。
看下ByteBuffer.allocate這個方法:

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

直接new了一個HeapByteBuffer對象,下面看下這個構造方法:

    HeapByteBuffer(int cap, int lim) {            // package-private

        super(-1, 0, lim, cap, new byte[cap], 0);
        /*
        hb = new byte[cap];
        offset = 0;
        */
    }

構造方法中調用了父類,ByteBuffer的構造方法

super(-1, 0, lim, cap, new byte[cap], 0);

其中new byte[cap],這裏就能夠確定,HeapByteBuffer申請的內存,確實是在jvm堆上。

DirectByteBuffer

在jvm堆外(OS堆上)申請的內存。通過ByteBuffer的allocateDirect申請。
看下ByteBuffer的allocateDirect方法:

    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

直接new 了一個DirectByteBuffer對象,看下DirectByteBuffer的構造方法

    DirectByteBuffer(int cap) {                   // package-private

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

其中申請內存的操作是通過unsafe類進行的

base = unsafe.allocateMemory(size);

Unsafe的allocateMemory方法通過c的malloc方法【底層依賴兩個系統調用,一個brk,一個mmap】進行進程堆上內存的分配。下面,我們驗證下這個想法:
這裏用到了幾個涉及到jvm監控和linux內存監控的知識:

  • Native Memory Tracking(NMT) jdk7提供的內存工具,跟蹤JVM內部的內存使用
  • linux的/proc/pid/maps 文件,可以查看進程的虛擬地址空間是如何使用的。

這裏不對這兩塊知識進行再補充,只注重分析內存情況。

測試代碼:

    public static void main(String[] args) throws Exception {
        ByteBuffer b = ByteBuffer.allocateDirect(1024*1024*50);
        //反射獲取Buffer中 的address屬性
        Field field = b.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("address");
        //打開私有訪問
        field.setAccessible(true);
        System.out.println(field.get(b).toString());
        while (true) {
            Thread.sleep(10000);
        }
    }

代碼中申請了50M的堆外內存。而且會打印出native代碼申請的內存的地址起始地址。
java命令參數如下:注意下-Xms20m -Xmx100m

java -Djava.rmi.server.hostname=49.234.60.90 -Dcom.sun.management.jmxremote.port=2990 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -XX:NativeMemoryTracking=detail -Xms20m -Xmx100m  NonBlockServer

在這裏插入圖片描述
這裏打印出了申請的地址的虛擬內存地址起始地址:140260761661472,轉換成16進制是,7f9100dff020

下面看下NMT的情況:
着重分析下jvm堆內存情況:
在這裏插入圖片描述
這裏,說明java堆內存申請了100M,當前使用的爲20M。
下面看具體的堆內存信息:
在這裏插入圖片描述
這裏堆內存的虛擬內存地址爲: 0x00000000f9c00000 - 0x0000000100000000

下面看下,linux的java進程的內存情況(通過查看/proc/pid/maps 文件):
在這裏插入圖片描述
上文中,我們打印的申請的內存的起始地址爲7f9100dff020。
乍一看,maps文件並沒有這個起始地址,但是注意這樣一個內存塊:7f9100dff000-7f9104000000。7f9100dff020剛好在這個區間中。但是7f9100dff000-7f9100dff020這塊多申請的內存,不知道是做什麼用的。

至此,我們驗證了,ByteBuffer的allocateDirect申請的內存並不在jvm堆上,而是在進程堆內存中。

MappedByteBuffer

java對內存文件映射的支持。
內存文件映射的原理:
普通的read io操作原理:
在這裏插入圖片描述
mmap操作的原理:
在這裏插入圖片描述
mmap的核心是,通過使得內核空間和用戶空間的虛擬地址映射到同一塊物理內存上,進而減少了文件在內核空間和用戶空間的拷貝。

下面用一個例子,簡單看下,MappedByteBuffer在內存文件映射操作時的java進程內存和jvm內存情況.

   public static void main(String[] args) throws Exception {
        String path = ModelDubboService.class.getClassLoader().getResource("test.txt").getPath();
        File file = new File(path);
        FileChannel fileChannel = new FileInputStream(file).getChannel();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
        //反射獲取Buffer中 的address屬性
        Field field = mappedByteBuffer.getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("address");
        //打開私有訪問
        field.setAccessible(true);
        System.out.println(field.get(mappedByteBuffer).toString());

        String path2 = ModelDubboService.class.getClassLoader().getResource("test2.txt").getPath();
        FileInputStream fileInputStream = new FileInputStream(file);
        byte[] bytes = new byte[1024*2014*40];
        fileInputStream.read(bytes);

        while(true){
            Thread.sleep(10000);
        }
    }

主要是看下,通過普通的讀文件操作和mmap方式讀文件有什麼區別。test.txt使用內存映射的方式來讀,test2.txt使用普通的read方法來讀。
java命令參數如下

java -Djava.rmi.server.hostname=49.234.60.90 -Dcom.sun.management.jmxremote.port=2990 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -XX:NativeMemoryTracking=detail -Xms20m -Xmx100m  TestMMap

獲取打印的內存地址:
在這裏插入圖片描述
地址139907108438016,轉換成16進制,7f3ea9800000

下面看下jvm堆內存情況:
在這裏插入圖片描述
jvm最大內存是200M,使用了166M左右。
在這裏插入圖片描述
獲取jvm堆內存的虛擬地址:0x00000000f3800000 - 0x0000000100000000

下面看下linux進程的內存(/proc/pid/maps):
在這裏插入圖片描述
爲了再驗證下,到底是不是通過mmap讀取的文件,我們再看下/proc/pid/map_files這個文件:
在這裏插入圖片描述
這裏就可以確認,test.txt確實是通過mmap讀取的,而text2.txt通過一般的read來讀取的,已經從pageCache拷貝到jvm堆上了。

這裏總結幾個java通過mmap方式相比於普通read方式的有點:

  • 不使用jvm堆內存,使用堆外內存,不會對jvm內存造成很大影響
  • mmap方式相比read,減少了一次拷貝操作(內核空間->用戶空間),速度更加快

同樣也有一些需要注意的地方:

  • mmap使用的堆外內存的回收問題
  • mmap適用於不修改文件的場景,如果需要修改文件,則還是要把文件拷貝到jvm堆內存中去操作
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章