Netty原始碼分析之ByteBuf引用計數

bigfan發表於2021-12-30

引用計數是一種常用的記憶體管理機制,是指將資源的被引用次數儲存起來,當被引用次數變為零時就將其釋放的過程。Netty在4.x版本開始使用引用計數機制進行部分物件的管理,其實現思路並不是特別複雜,它主要涉及跟蹤某個物件被引用的次數。在Netty具體程式碼中需要通過引用計數進行記憶體管理的物件,會基於ReferenceCounted介面實現,其中引用計數大於0時則代表該物件被引用不會釋放,當引用計數減少到0時,該物件就會被釋放。通過引用計數機制,Netty可以很好的實現記憶體管理,引用計數減少到0時要麼直接釋放記憶體,要麼放回記憶體池中重複利用。

1、基本示例

下面先通過一個簡單示例看下Netty中引用計數機制的使用

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        
           ByteBuf recvBuffer = (ByteBuf) msg;// 申請ByteBuf 需要主動釋放
        if(recvBuffer.isDirect()){
            System.err.println(true);
        }
        PooledByteBufAllocator allocator = new PooledByteBufAllocator(true);
        ByteBuf sendBuffer = allocator.buffer();//申請池化直接記憶體
        System.err.println("sendBuffer的引用計數:"+sendBuffer.refCnt());
        sendBuffer.retain();
        System.err.println("sendBuffer的引用計數:"+sendBuffer.refCnt());
        sendBuffer.release();
        System.err.println("sendBuffer的引用計數:"+sendBuffer.refCnt());
try { byte[] bytesReady = new byte[recvBuffer.readableBytes()]; recvBuffer.readBytes(bytesReady); System.out.println("channelRead收到資料:"+ BytesUtils.toHexString(bytesReady)); byte[] sendBytes = new byte[] {0x7E,0x01,0x02,0x7e}; sendBuffer.writeBytes(sendBytes); ctx.writeAndFlush(sendBuffer); System.err.println("sendBuffer的引用計數:"+sendBuffer.refCnt()); }catch (Exception e) { // TODO: handle exception System.err.println(e.getMessage()); }finally { System.err.println("recvBuffer的引用計數:"+recvBuffer.refCnt()); recvBuffer.release(); //此處需要釋放 System.err.println("recvBuffer的引用計數:"+recvBuffer.refCnt()); } }

輸出結果如下,通過示例可以看出retain方法會增加計數引用,release方法會減少計數引用

true
sendBuffer的引用計數:1
sendBuffer的引用計數:2
sendBuffer的引用計數:1
sendBuffer的引用計數:0
recvBuffer的引用計數:1
recvBuffer的引用計數:0

AbstractReferenceCountedByteBuf實現了對ByteBuf的記憶體管理,以實現記憶體的回收、釋放或者重複利用 ,AbstractReferenceCountedByteBuf的繼承實現關係如下圖所示

2、ReferenceCounted介面定義

首先是ReferenceCounted介面的定義

public interface ReferenceCounted {
    /**
     * Returns the reference count of this object.  If {@code 0}, it means this object has been deallocated.
     * 返回物件的引用計數
     */
    int refCnt();

    /**
     * Increases the reference count by {@code 1}.
     * 增加引用計數
     */
    ReferenceCounted retain();

    /**
     * Increases the reference count by the specified {@code increment}.
     * 引用計數增加指定值
     */
    ReferenceCounted retain(int increment);

    /**
     * Records the current access location of this object for debugging purposes.
     * If this object is determined to be leaked, the information recorded by this operation will be provided to you
     * via {@link ResourceLeakDetector}.  This method is a shortcut to {@link #touch(Object) touch(null)}.
     * 記錄該物件的當前訪問位置,用於除錯。
     * 如果確定該物件被洩露,將提供此操作記錄的資訊給您
     */
    ReferenceCounted touch();

    /**
     * Records the current access location of this object with an additional arbitrary information for debugging
     * purposes.  If this object is determined to be leaked, the information recorded by this operation will be
     * provided to you via {@link ResourceLeakDetector}.
     * 記錄該物件的當前訪問位置,附加資訊用於除錯。
     * 如果確定該物件被洩露,將提供此操作記錄的資訊給您
     */
    ReferenceCounted touch(Object hint);

    /**
     * Decreases the reference count by {@code 1} and deallocates this object if the reference count reaches at
     * {@code 0}.
     *
     * @return {@code true} if and only if the reference count became {@code 0} and this object has been deallocated
     * 引用計數減少,如果計數變為了0,則釋放物件資源
     * 如果物件資源被釋放,則返回true,否則返回false
     */
    boolean release();

    /**
     * Decreases the reference count by the specified {@code decrement} and deallocates this object if the reference
     * count reaches at {@code 0}.
     *
     * @return {@code true} if and only if the reference count became {@code 0} and this object has been deallocated
     * 引用計數-指定值,如果計數變為了0,則釋放物件資源或交回到物件池
     * 如果物件資源被釋放,則返回true,否則返回false
     */
    boolean release(int decrement);
}

 

3、AbstractReferenceCountedByteBuf原始碼分析

AbstractReferenceCountedByteBuf對ReferenceCounted進行了具體實現,retain與release兩個方法通過CAS方式對引用計數refcnt進行操作,下面對其原始碼進行簡單分析

初始化

引用計數初始值refCnt 使用關鍵字volatile修飾,保證執行緒的可見性,同時使用偶數,引用增加通過位移操作實現,提高運算效率。

採用 AtomicIntegerFieldUpdater 物件,通過CAS方式更新refCnt,以實現執行緒安全,避免加鎖,提高效率。

    private static final long REFCNT_FIELD_OFFSET;
    //採用 AtomicIntegerFieldUpdater 物件,CAS方式更新refCnt
    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
            AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");

    //refCnt 實際值為偶數,採用位移操作提高效率
    // even => "real" refcount is (refCnt >>> 1); odd => "real" refcount is 0
    @SuppressWarnings("unused")
    private volatile int refCnt = 2;

retain操作

上面示例中每呼叫一次retain方法,引用計數就會累加一次,我們看下原始碼中retain的具體實現

    public ByteBuf retain() {
        return retain0(1);
    }

    @Override
    public ByteBuf retain(int increment) {
        return retain0(checkPositive(increment, "increment"));
    }

    //計數器增值操作
    private ByteBuf retain0(final int increment) {
        // all changes to the raw count are 2x the "real" change
        int adjustedIncrement = increment << 1; // overflow OK here  真正的計數都是2倍遞增
        int oldRef = refCntUpdater.getAndAdd(this, adjustedIncrement); //通過CAS方式遞增並獲取原值
        if ((oldRef & 1) != 0) {//判斷奇偶,正常情況這裡應該都是偶數
            throw new IllegalReferenceCountException(0, increment);
        }
        // don't pass 0!  如果計數小於等於0,以及整型範圍越界(0x7fffffff+1)丟擲異常
        if ((oldRef <= 0 && oldRef + adjustedIncrement >= 0)
                || (oldRef >= 0 && oldRef + adjustedIncrement < oldRef)) {
            // overflow case
            refCntUpdater.getAndAdd(this, -adjustedIncrement);
            throw new IllegalReferenceCountException(realRefCnt(oldRef), increment);
        }
        return this;
    }

release操作

通過呼叫release方法,對引用計數做減值操作,原始碼中release的具體實現要注意的是由於引用計數以2倍遞增,所以引用次數= 引用計數/2,當decrement=refcnt/2 也就是引用次數=釋放次數時,代表ByteBuf不再被引用,執行記憶體釋放或放回記憶體池的操作。

    //計數器減值操作
    private boolean release0(int decrement) {
        int rawCnt = nonVolatileRawCnt(), realCnt = toLiveRealCnt(rawCnt, decrement); //對計數器進行除以2操作,也就是引用次數
        /**
         * /這裡如注意 你傳入的減值引數decrement  = realCnt 時 等同於 引用次數=釋放次數,直接進行釋放操作
         */
        if (decrement == realCnt) {
            if (refCntUpdater.compareAndSet(this, rawCnt, 1)) { //CAS方式置為1
                deallocate();//記憶體釋放或放回記憶體池
                return true;
            }
            return retryRelease0(decrement);//進入具體操作
        }
        return releaseNonFinal0(decrement, rawCnt, realCnt);
    }

    private boolean releaseNonFinal0(int decrement, int rawCnt, int realCnt) {
        //如果decrement 小於 realCnt,通過CAS方式減去decrement*2
        if (decrement < realCnt
                // all changes to the raw count are 2x the "real" change
                && refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) {
            return false;
        }
        return retryRelease0(decrement);
    }

    private boolean retryRelease0(int decrement) {
        for (;;) {
            int rawCnt = refCntUpdater.get(this), realCnt = toLiveRealCnt(rawCnt, decrement);
            if (decrement == realCnt) {
                if (refCntUpdater.compareAndSet(this, rawCnt, 1)) {
                    deallocate();
                    return true;
                }
            } else if (decrement < realCnt) {//如果decrement 小於 realCnt,通過CAS方式減去decrement*2
                // all changes to the raw count are 2x the "real" change
                if (refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) {
                    return false;
                }
            } else {
                throw new IllegalReferenceCountException(realCnt, -decrement);
            }
            Thread.yield(); // this benefits throughput under high contention
        }
    }

    /**
     * Like {@link #realRefCnt(int)} but throws if refCnt == 0
     */
    private static int toLiveRealCnt(int rawCnt, int decrement) {
        if ((rawCnt & 1) == 0) {
            return rawCnt >>> 1;
        }
        // odd rawCnt => already deallocated
        throw new IllegalReferenceCountException(0, -decrement);
    }

4、總結 

以上我們圍繞AbstractReferenceCountedByteBuf對Netty引用計數的具體實現進行了分析,可以看到Netty在實現引用計數的同時,結合CAS、位移計算等方式,保證了運算效率和執行緒安全,在實際專案中我們遇到類似應用場景也都可以借鑑參考,如資料傳送次數,商品剩餘數量等計數場景的實現。希望本文對大家能有所幫助,其中如有不足與不正確的地方還望指正與海涵,十分感謝。

 

關注微信公眾號,檢視更多技術文章。

 

 

相關文章