Buffer的建立及使用原始碼分析——ByteBuffer為例

bmilk發表於2020-07-06

目錄

  • Buffer概述
  • Buffer的建立
  • Buffer的使用
  • 總結
  • 參考資料

Buffer概述

注:全文以ByteBuffer類為例說明
Java中提供了7種型別的Buffer,每一種型別的Buffer根據分配記憶體的方式不同又可以分為
直接緩衝區和非直接緩衝區。

Buffer的本質是一個定長陣列,並且在建立的時候需要指明Buffer的容量(陣列的長度)。
而這個陣列定義在不同的Buffer當中。例如ByteBuffer的定義如下:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
{

    // These fields are declared here rather than in Heap-X-Buffer in order to
    // reduce the number of virtual method invocations needed to access these
    // values, which is especially costly when coding small buffers.
    //
    //在這裡定義Buffer對應的陣列,而不是在Heap-X-Buffer中定義
    //目的是為了減少訪問這些紙所需的虛方法呼叫,但是對於小的緩衝區,代價比較高
    final byte[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers

    // Creates a new buffer with the given mark, position, limit, capacity,
    // backing array, and array offset
    //
    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                 byte[] hb, int offset)
    {
        //呼叫父類Buffer類的建構函式構造
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

    // Creates a new buffer with the given mark, position, limit, and capacity
    //
    ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
        this(mark, pos, lim, cap, null, 0);
    }
......
}

儘管陣列在這裡定義,但是這個陣列只對非直接緩衝區有效。

ByteBuffer類有兩個子類分別是:DirectByteBuffer(直接緩衝區類)和HeapByteBuffer(非直接緩衝區)。
但是這兩個類並不能直接被訪問,因為這兩個類是包私有的,而建立這兩種緩衝區的方式就是通過呼叫Buffer
類提供的建立緩衝區的靜態方法:allocate()allocateDirect()

Buffer的建立

Buffer要麼是直接的要麼是非直接的,非直接緩衝區的記憶體分配在JVM記憶體當中,
而直接緩衝區使用實體記憶體對映,直接在實體記憶體中分配緩衝區,既然分配記憶體的地方不一樣,
BUffer的建立方式也就不一樣。

非直接緩衝區記憶體的分配

建立非直接緩衝區可以通過呼叫allocate()方法,這樣會將緩衝區建立在JVM記憶體(堆記憶體)當中。
allocate()方法是一個靜態方法,因此可以直接使用類來呼叫。
具體的建立過程如下:

    /**
     * Allocates a new byte buffer.
     *
     * <p> The new buffer's position will be zero, its limit will be its
     * capacity, its mark will be undefined, and each of its elements will be
     * initialized to zero.  It will have a {@link #array backing array},
     * and its {@link #arrayOffset array offset} will be zero.
     *
     * @param  capacity
     *         The new buffer's capacity, in bytes
     *
     * @return  The new byte buffer
     *
     * @throws  IllegalArgumentException
     *          If the <tt>capacity</tt> is a negative integer
     */
    //分配一個緩衝區,最後返回的其實是一個HeapByteBuffer的物件
    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        //這裡呼叫到HeapByteBuffer類的建構函式,建立非直接緩衝區
        //並將需要的Buffer容量傳遞
        //從名稱也可以看出,建立的位置在堆記憶體上。
        return new HeapByteBuffer(capacity, capacity);
    }

HeapByteBuffer(capacity, capacity)用於在堆記憶體上建立一個緩衝區。
該方法優惠調回ByteBuffer構造方法,HeapByteBuffer類沒有任何的欄位,他所需的欄位全部定義在父類當中。
原始碼分析如下:

    HeapByteBuffer(int cap, int lim) {
        // 呼叫父類的構造方法建立非直接緩衝區           // package-private
        // 呼叫時根據傳遞的容量建立了一個陣列。
        super(-1, 0, lim, cap, new byte[cap], 0);
    }
    
    //ByteBuffer類的構造方法,也就是上面程式碼呼叫的super方法
    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                     byte[] hb, int offset)
        {
            //接著呼叫Buffer類的構造方法給用於運算元組的四個屬性賦值
            super(mark, pos, lim, cap);
            //將陣列賦值給ByteBuffer的hb屬性,
            this.hb = hb;
            this.offset = offset;
        }

    //Buffer類的構造方法
    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        //容量引數校驗,原始容量不能小於0
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        //設定容量
        this.capacity = cap;
        //這裡的lim從上面傳遞過來的時候就是陣列的容量
        //limit在寫模式下預設可操作的範圍就是整個陣列
        //limit在讀模式下可以操作的範圍是陣列中寫入的元素
        //建立的時候就是寫模式,是整個陣列
        limit(lim);
        //初始的position是0
        position(pos);
        //設定mark的值,初始情況下是-1,因此有一個引數校驗,
        //-1是陣列之外的下標,不可以使用reset方法使得postion到mark的位置。
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }

在堆上建立緩衝區還是很簡單的,本質就是建立了一個陣列以及一些用於輔助運算元組的其他屬性。

最後返回的其實是一個HeapByteBuffer的物件,因此對其的後續操作大多應該是要呼叫到HeapByteBuffer類中

直接緩衝區的建立

建立直接俄緩衝區可以通過呼叫allocateDirect()方法建立,原始碼如下:

    /**
     * Allocates a new direct byte buffer.
     *
     * <p> The new buffer's position will be zero, its limit will be its
     * capacity, its mark will be undefined, and each of its elements will be
     * initialized to zero.  Whether or not it has a
     * {@link #hasArray backing array} is unspecified.
     *
     * @param  capacity
     *         The new buffer's capacity, in bytes
     *
     * @return  The new byte buffer
     *
     * @throws  IllegalArgumentException
     *          If the <tt>capacity</tt> is a negative integer
     */
    //建立一個直接緩衝區
    public static ByteBuffer allocateDirect(int capacity) {
        //同非直接緩衝區,都是建立的子類的物件
        //建立一個直接緩衝區物件
        return new DirectByteBuffer(capacity);
    }

DirectByteBuffer(capacity)DirectByteBuffer的建構函式,具體程式碼如下:

    DirectByteBuffer(int cap) {                   // package-private
        //初始化mark,position,limit,capacity
        super(-1, 0, cap, cap);
        //記憶體是否按頁分配對齊,是的話,則實際申請的記憶體可能會增加達到對齊效果
        //預設關閉,可以通過-XX:+PageAlignDirectMemory控制
        boolean pa = VM.isDirectMemoryPageAligned();
        //獲取每頁記憶體的大小
        int ps = Bits.pageSize();
        //分配記憶體的大小,如果是按頁對其的方式,需要加一頁記憶體的容量
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        //預定記憶體,預定不到則進行回收堆外記憶體,再預定不到則進行Full gc
        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;
        }
        /**
         *建立堆外記憶體回收Cleanner,Cleanner物件是一個PhantomFerence幽靈引用,
         *DirectByteBuffer物件的堆記憶體回收了之後,幽靈引用Cleanner會通知Reference
         *物件的守護程式ReferenceHandler對其堆外記憶體進行回收,呼叫Cleanner的
         *clean方法,clean方法呼叫的是Deallocator物件的run方法,run方法呼叫的是
         *unsafe.freeMemory回收堆外記憶體。
        
         *堆外記憶體minor gc和full gc的時候都不會進行回收,而是ReferenceHandle守護程式呼叫
         *cleanner物件的clean方法進行回收。只不過gc 回收了DirectByteBuffer之後,gc會通知Cleanner進行回收
         */
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;

    }

由於是在實體記憶體中直接分配一塊記憶體,而java並不直接操作記憶體需要交給JDKnative方法的實現分配

Bits.reserveMemory(size, cap)預定記憶體原始碼,預定記憶體,說穿了就是檢查堆外記憶體是否足夠分配

    // These methods should be called whenever direct memory is allocated or
    // freed.  They allow the user to control the amount of direct memory
    // which a process may access.  All sizes are specified in bytes.
    // 在分配或釋放直接記憶體時應當呼叫這些方法,
    // 他們允許用控制程式可以訪問的直接記憶體的數量,所有大小都以位元組為單位
    static void reserveMemory(long size, int cap) {
        //memoryLimitSet的初始值為false
        //獲取允許的最大堆外記憶體賦值給maxMemory,預設為64MB
        //可以通過-XX:MaxDirectMemorySize引數控制
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }

        // optimist!
        //理想情況,maxMemory足夠分配(有足夠記憶體供預定)
        if (tryReserveMemory(size, cap)) {
            return;
        }

        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        // 這裡會嘗試回收堆外空間,每次回收成功嘗試進行堆外空間的引用
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

        // trigger VM's Reference processing
        // 依然分配失敗嘗試回收堆空間,觸發full gc
        // 
        System.gc();

        // a retry loop with exponential back-off delays
        // (this gives VM some time to do it's job)
        boolean interrupted = false;
        
        // 接下來會嘗試最多9次的記憶體預定,應該說是9次的回收堆外記憶體失敗的記憶體預定
        // 如果堆外記憶體回收成功,則直接嘗試一次記憶體預定,只有回收失敗才會sleep執行緒。
        // 每次預定的時間間隔為1ms,2ms,4ms,等2的冪遞增,最多256ms。
        try {
            long sleepTime = 1;
            int sleeps = 0;
            while (true) {
                // 嘗試預定記憶體
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                // 預定記憶體失敗則進行嘗試釋放堆外記憶體,
                // 累計最高可以允許釋放堆外記憶體9次,同時sleep執行緒,對應時間以2的指數冪遞增
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }

            // no luck
            throw new OutOfMemoryError("Direct buffer memory");

        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }

為什麼呼叫System.gc?引用自JVM原始碼分析之堆外記憶體完全解讀

既然要呼叫System.gc,那肯定是想通過觸發一次gc操作來回收堆外部記憶體,不過我想先說的是堆外部記憶體不會對gc造成什麼影響(這裡的System.gc除外),但是堆外層記憶體的回收實際上依賴於我們的gc機制,首先我們要知道在java尺寸和我們在堆外分配的這塊記憶體分配的只有與之關聯的DirectByteBuffer物件了,它記錄了這塊記憶體的基地址以及大小,那麼既然和gc也有關,那就是gc能通過DirectByteBuffer物件來間接操作對應的堆外部記憶體了。DirectByteBuffer物件在建立的時候關聯了一個PhantomReference,說到PhantomReference時被回收的,它不能影響gc方法,但是gc過程中如果發現某個物件只有只有PhantomReference引用它之外,並沒有其他的地方引用它了,那將會把這個引用放到java.lang.ref .Reference.pending物理裡,在gc完成的時候通知ReferenceHandler這個守護執行緒去執行一些後置處理,而DirectByteBuffer關聯的PhantomReferencePhantomReference的一個子類,在最終的處理裡會通過Unsafe的免費介面來釋放DirectByteBuffer對應的堆外記憶體塊

Buffer的使用

切換讀模式flip()

切換為讀模式的程式碼分廠簡單,就是使limit指標指向buffer中最後一個插入的元素的位置,即position,指標的位置。
position代表操作的位置,那麼從0開始,所以需要將position指標歸0.原始碼如下:

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

get()讀取

get()讀取的核心是緩衝區對應的陣列中取出元素放在目標陣列中(get(byte[] dst)方法是有一個引數的,傳入的就是目標陣列)。

    public ByteBuffer get(byte[] dst) {
        return get(dst, 0, dst.length);
    }

    public ByteBuffer get(byte[] dst, int offset, int length) {
        checkBounds(offset, length, dst.length);
        if (length > remaining())
            throw new BufferUnderflowException();
        int end = offset + length;
        //shiyongfor迴圈依次放入目標陣列中
        for (int i = offset; i < end; i++)
            // get()對於直接緩衝區和非直接緩衝區是不一樣的,所以交由子類實現。
            dst[i] = get();
        return this;
    }

rewind()重複讀

既然要重複讀就需要把position置0了

    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

clear()清空緩衝區與compact()方法

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

clear()方法中,僅僅是將三個指標還原為建立時的狀態供後續寫入,但是之前寫入的資料並沒有被刪除,依然可以使用get(int index)獲取

但是有一種情況,緩衝區已經滿了還想接著寫入,但是沒有讀取完又不能從頭開始寫入該怎麼辦,答案是compact()方法

非直接緩衝區:
   public ByteBuffer compact() {
        //將未讀取的部分拷貝到緩衝區的最前方
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        //設定position位置到緩衝區下一個可以寫入的位置
        position(remaining());
        //設定limit是最大容量
        limit(capacity());
        //設定mark=-1
        discardMark();
        return this;
    }

直接緩衝區:
    public ByteBuffer compact() {    
        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        //呼叫native方法拷貝未讀物部分
        unsafe.copyMemory(ix(pos), ix(0), (long)rem << 0);
        //設定指標位置
        position(rem);
        limit(capacity());
        discardMark();
        return this;
    }

mark()標記位置以及reset()還原

mark()標記一個位置,準確的說是當前的position位置

    public final Buffer mark() {
        mark = position;
        return this;
    }

標記了之後並不影響寫入或者讀取,position指標從這個位置離開再次想從這個位置讀取或者寫入時,
可以使用reset()方法

    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

總結

本文其實還有很多不清楚的地方,對於虛引用以及引用佇列的操作還不是很清楚去,對於虛引用和堆外記憶體的回收的關係原始碼其實也沒看到,
需要再看吧,寫這篇的目的其實最開始就是想研究看看直接緩衝區記憶體的分配,沒想到依然糊塗,後面填坑。路過的大佬也就指導下虛引用這部分相關的東西,謝謝。

參考資料

相關文章