7.netty內(nèi)存管理-ByteBuf

ByteBuf

ByteBuf是什么

為了平衡數(shù)據(jù)傳輸時(shí)CPU與各種IO設(shè)備速度的差異性,計(jì)算機(jī)設(shè)計(jì)者引入了緩沖區(qū)這一重要抽象纱烘。jdkNIO庫提供了java.nio.Buffer接口杨拐,并且提供了7種默認(rèn)實(shí)現(xiàn),常見的實(shí)現(xiàn)類為ByteBuffer擂啥。不過netty并沒有直接使用nio的ByteBuffer哄陶,這主要是由于jdk的Buffer有以下幾個(gè)缺點(diǎn):

  1. 當(dāng)調(diào)用allocate方法分配內(nèi)存時(shí),Buffer的長度就固定了哺壶,不能動(dòng)態(tài)擴(kuò)展和收縮屋吨,當(dāng)寫入數(shù)據(jù)大于緩沖區(qū)的capacity時(shí)會(huì)發(fā)生數(shù)組越界錯(cuò)誤
  2. Buffer只有一個(gè)位置標(biāo)志位屬性position,讀寫切換時(shí)山宾,必須先調(diào)用flip或rewind方法至扰。不僅如此,因?yàn)閒lip的切換
  3. Buffer只提供了存取资锰、翻轉(zhuǎn)敢课、釋放、標(biāo)志绷杜、比較直秆、批量移動(dòng)等緩沖區(qū)的基本操作,想使用高級的功能(比如池化),就得自己手動(dòng)進(jìn)行封裝及維護(hù)鞭盟,使用非常不方便圾结。
    也因此,netty實(shí)現(xiàn)了自己的緩沖區(qū)——ByteBuf,連名字都如此相似齿诉。那么ByteBuf是如何規(guī)避ByteBuffer的缺點(diǎn)的筝野?
    第一點(diǎn)顯然是很好解決的,由于ByteBuf底層也是數(shù)組,那么它就可以像ArrayList一樣鹃两,在寫入操作時(shí)進(jìn)行容量檢查遗座,當(dāng)容量不足時(shí)進(jìn)行擴(kuò)容。
    第二點(diǎn)俊扳,ByteBuf通過2個(gè)索引readerIndex,writerIndex將數(shù)組分為3部分猛遍,如下圖所示
+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
|                   |     (CONTENT)    |                  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0      <=      readerIndex   <=   writerIndex    <=    capacity

初始化時(shí)馋记,readerIndex和writerIndex都是0号坡,隨著數(shù)據(jù)的寫入writerIndex會(huì)增加,此時(shí)readable byte部分增加梯醒,writable bytes減少宽堆。當(dāng)讀取時(shí),discardable bytes增加茸习,readable bytes減少畜隶。由于讀操作只修改readerIndex,寫操作只修改writerIndex号胚,讓ByteBuf的使用更加容易理解籽慢,避免了由于遺漏flip導(dǎo)致的功能異常。
此外猫胁,當(dāng)調(diào)用discardReadBytes方法時(shí)箱亿,可以把discardable bytes這部分的內(nèi)存釋放∑眩總體想法是通過將readerIndex移動(dòng)到0届惋,writerIndex移動(dòng)到writerIndex-readerIndex下標(biāo),具體移動(dòng)下標(biāo)的方式依據(jù)ByteBuf實(shí)現(xiàn)類有所不同菠赚。這個(gè)方法可以顯著提高緩沖區(qū)的空間復(fù)用率脑豹,避免無限度的擴(kuò)容,但會(huì)發(fā)生字節(jié)數(shù)組的內(nèi)存復(fù)制衡查,屬于以時(shí)間換空間的做法晨缴。

ByteBuf重要API

read、write峡捡、set击碗、skipBytes

前3個(gè)系列的方法及最后一個(gè)skipBytes都屬于改變指針的方法。舉例來說们拙,readByte會(huì)移動(dòng)readerIndex1個(gè)下標(biāo)位稍途,而int是4個(gè)byte的大小,所以readInt會(huì)移動(dòng)readerIndex4個(gè)下標(biāo)位砚婆,相應(yīng)的械拍,writeByte會(huì)移動(dòng)writerIndex1個(gè)下標(biāo)位,writeInt會(huì)移動(dòng)writerIndex4個(gè)下標(biāo)位装盯。set系列方法比較特殊坷虑,它的參數(shù)為index和value,意即將value寫入指定的index位置埂奈,但這個(gè)操作不會(huì)改變r(jià)eaderIndex和writerIndex迄损。skipBytes比較簡單粗暴,直接將readerIndex移動(dòng)指定長度账磺。

mark和reset

markReaderIndex和markWriterIndex可以將對應(yīng)的指針做一個(gè)標(biāo)記芹敌,當(dāng)需要重新操作這部分?jǐn)?shù)據(jù)時(shí)痊远,再使用resetReaderIndex或resetWriterIndex凳寺,將對應(yīng)指針復(fù)位到mark的位置翻诉。

duplicate剔猿、slice羡玛、copy

這3種方法都可以復(fù)制一份字節(jié)數(shù)組魄眉,不同之處在于duplicate和slice兩個(gè)方法返回的新ByteBuf和原有的老ByteBuf之間的內(nèi)容會(huì)互相影響票彪,而copy則不會(huì)扫步。duplicate和slice的區(qū)別在于前者復(fù)制整個(gè)ByteBuf的字節(jié)數(shù)組蝙云,而后者默認(rèn)僅復(fù)制可讀部分捆等,但可以通過slice(index, length)分割指定的區(qū)間滞造。

retain、release

這是ByteBuf接口繼承自ReferenceCounted接口的方法楚里,用于引用計(jì)數(shù)断部,以便在不使用對象時(shí)及時(shí)釋放。實(shí)現(xiàn)思路是當(dāng)需要使用一個(gè)對象時(shí)班缎,計(jì)數(shù)加1蝴光;不再使用時(shí),計(jì)數(shù)減1达址∶锼睿考慮到多線程場景,一般也多采用AtomicInteger實(shí)現(xiàn)沉唠。netty卻另辟蹊徑疆虚,選擇了volatile + AtomicIntegerFieldUpdater這樣一種更節(jié)省內(nèi)存的方式。

ByteBuf擴(kuò)容

在ByteBuf寫入數(shù)據(jù)時(shí)會(huì)檢查可寫入的容量满葛,若容量不足會(huì)進(jìn)行擴(kuò)容径簿。

final void ensureWritable0(int minWritableBytes) {
    if (minWritableBytes <= writableBytes()) {
        return;
    }
    int minNewCapacity = writerIndex + minWritableBytes;
    int newCapacity = alloc().calculateNewCapacity(minNewCapacity, maxCapacity);
    int fastCapacity = writerIndex + maxFastWritableBytes();
    if (newCapacity > fastCapacity && minNewCapacity <= fastCapacity) {
        newCapacity = fastCapacity;
    }
    capacity(newCapacity);
}

忽略一些檢驗(yàn)性質(zhì)的代碼后,可以看到擴(kuò)容時(shí)先嘗試將現(xiàn)有寫索引加上需要寫入的容量大小作為最小新容量嘀韧,并調(diào)用ByteBufAllocate的calculateNewCapacity方法進(jìn)行計(jì)算篇亭。跟入這個(gè)方法:

public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
    final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
    if (minNewCapacity == threshold) {
        return threshold;
    }
    if (minNewCapacity > threshold) {
        int newCapacity = minNewCapacity / threshold * threshold;
        if (newCapacity > maxCapacity - threshold) {
            newCapacity = maxCapacity;
        } else {
            newCapacity += threshold;
        }
        return newCapacity;
    }
    int newCapacity = 64;
    while (newCapacity < minNewCapacity) {
        newCapacity <<= 1;
    }
    return Math.min(newCapacity, maxCapacity);
}

可以看到這個(gè)方法的目的則是計(jì)算比可寫容量稍大的2的冪次方。minNewCapacity由上一個(gè)方法傳入锄贷,而maxCapacity則為Integer.MAX_VALUE译蒂。具體步驟是首先判斷新容量minNewCapacity是否超過了計(jì)算限制CALCULATE_THRESHOLD,默認(rèn)為4M谊却,如果沒有超過4MB柔昼,那么從64B開始不斷以2的冪次方形式擴(kuò)容,直到newCapacity超過minNewCapacity炎辨。而若一開始新容量就超過了4M捕透,則調(diào)整新容量到4M的倍數(shù)+1。比如newCapacity為6M,因?yàn)?/4 = 1,所以調(diào)整為(1+1)*4M=8M激率。

在計(jì)算完容量之后會(huì)調(diào)用capacity方法咳燕。這是一個(gè)抽象方法勿决,這里以UnpooledHeapByteBuf為例乒躺。

public ByteBuf capacity(int newCapacity) {
    checkNewCapacity(newCapacity);
    byte[] oldArray = array;
    int oldCapacity = oldArray.length;
    if (newCapacity == oldCapacity) {
        return this;
    }
    int bytesToCopy;
    if (newCapacity > oldCapacity) {
        bytesToCopy = oldCapacity;
    } else {
        trimIndicesToCapacity(newCapacity);
        bytesToCopy = newCapacity;
    }
    byte[] newArray = allocateArray(newCapacity);
    System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy);
    setArray(newArray);
    freeArray(oldArray);
    return this;
}

首先檢查newCapacity是否大于0且小于最大容量。之后準(zhǔn)備好老數(shù)組要復(fù)制的長度低缩。trimIndicesToCapacity(newCapacity)是縮容時(shí)調(diào)用的嘉冒,它將readerIndex和newCapacity的較小值設(shè)置為新的readerIndex,將newCapacity設(shè)置為新的writerIndex咆繁。
之后便分配一個(gè)新數(shù)組讳推,并開始復(fù)制舊數(shù)組的元素。復(fù)制成功后玩般,將新數(shù)組保存為成員變量银觅,將老數(shù)組釋放掉。

ByteBuf種類

出于性能和空間的多方考慮坏为,netty從3個(gè)維度定義了各種不同的ByteBuf實(shí)現(xiàn)類究驴,主要是池化、堆內(nèi)堆外匀伏、可否使用Unsafe類這3個(gè)維度洒忧,從而演化出8種不同的ByteBuf,它們分別是PooledUnsafeHeapBytebuf够颠、PooledHeapByteBuf熙侍、PooledUnsafeDirectByteBuf、PooledDirectBytebuf履磨、UnpooledUnsafeHeapByteBuf蛉抓、UnpooledHeapByteBuf、UnpooledUnsafeDirectByteBuf剃诅、UnpooledDirectByteBuf巷送。
ByteBuf接口之下有一個(gè)抽象類AbstractByteBuf,實(shí)現(xiàn)了接口定義的read综苔、write惩系、set相關(guān)的方法,但在實(shí)現(xiàn)時(shí)只做了檢查如筛,而具體邏輯則定義一系列以_開頭的proteced方法堡牡,留待子類實(shí)現(xiàn)。

ByteBufAllocate

不同于一般形式的創(chuàng)建對象杨刨,ByteBuf需要通過內(nèi)存分配器ByteBufAllocate分配晤柄,對應(yīng)于不同的ByteBuf也會(huì)有不同的BtteBufferAllocate。netty將之抽象為ByteBufAllocate接口妖胀。我們看一下有哪些方法:

  1. buffer()芥颈、buffer(initialCapacity)惠勒、buffer(initialCapacity、maxCapacity)爬坑,分配ByteBuf的方法纠屋,具體分配的Buffer是堆內(nèi)還是堆外則由實(shí)現(xiàn)類決定。2個(gè)重載方法分別以給定初始容量盾计、最大容量的方式分配內(nèi)存
  2. ioBuffer()售担、ioBuffer(initialCapacity)、ioBuffer(initialCapacity署辉、maxCapacity)更傾向于分配堆外內(nèi)存的方法族铆,因?yàn)槎淹鈨?nèi)存更適合用于IO操作。重載方法同上
  3. heapBuffer()哭尝、heapBuffer(initialCapacity)哥攘、heapBuffer(initialCapacity、maxCapacity)分配堆內(nèi)內(nèi)存的方法材鹦。
  4. directBuffer()逝淹、directBuffer(initialCapacity)、directBuffer(initialCapacity侠姑、maxCapacity)分配堆外內(nèi)存的方法创橄。
  5. compositeBuffer()∶Ш欤可以將多個(gè)ByteBuf合并為一個(gè)ByteBuf妥畏,多個(gè)ByteBuf可以部分是堆內(nèi)內(nèi)存,部分是堆外內(nèi)存安吁。
    ByteBufAllocate接口定義了heap和direct這一個(gè)維度醉蚁,其他維度則交由子類來定義。

UnPooledByteBufAllocate

ByteBufAllocate有一個(gè)直接實(shí)現(xiàn)類AbstractByteBufAllocate鬼店,它實(shí)現(xiàn)了大部分方法网棍,只留下2個(gè)抽象方法newHeapBuffer和newDirectBuffer交由子類實(shí)現(xiàn)。AbstractByteBufAllocate有2個(gè)子類PooledByteBufAllocate和UnpooledByteBufAllocate妇智,在這里定義了pooled池化維度的分配方式滥玷。
看看UnpooledByteBufAllocate如何實(shí)現(xiàn)2個(gè)抽象方法:

newHeapBuffer

protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
    return PlatformDependent.hasUnsafe() ?
            new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
            new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}

可以看到實(shí)現(xiàn)類根據(jù)PlatformDependent.hasUnsafe()方法自動(dòng)判定是否使用unsafe維度,這個(gè)方法通過在靜態(tài)代碼塊中嘗試初始化sun.misc.Unsafe來判斷Unsafe類是否在當(dāng)前平臺(tái)可用巍棱,在juc中惑畴,這個(gè)類使用頗多,作為與高并發(fā)打交道的netty航徙,出現(xiàn)這個(gè)類不令人意外如贷。UnpooledUnsafeHeapByteBuf與UnpooledHeapByteBuf并不是平級關(guān)系,事實(shí)上前者繼承了后者,在構(gòu)造方法上也直接調(diào)用UnpooledHeapByteBuf的構(gòu)造方法杠袱。構(gòu)造方法比較簡單尚猿,初始化byte數(shù)組、初始容量楣富、最大容量凿掂,將讀寫指針的設(shè)置為0,并將子類傳入的this指針保存到alloc變量中菩彬。
兩種Bytebuf的區(qū)別在于unsafe會(huì)嘗試通過反射的方式創(chuàng)建byte數(shù)組缠劝,并將數(shù)組的地址保存起來潮梯,之后再獲取數(shù)據(jù)時(shí)也會(huì)調(diào)用Unsafe的getByte方法骗灶,通過數(shù)組在內(nèi)存中的地址+偏移量的形式直接獲取,而普通的SafeByteBuf則是保存byte數(shù)組秉馏,通過數(shù)組索引即array[index]訪問耙旦。

// UnsafeHeapByteBuf初始化數(shù)組
protected byte[] allocateArray(int initialCapacity) {
    return PlatformDependent.allocateUninitializedArray(initialCapacity);
}
// HeapByteBuf初始化數(shù)組
protected byte[] allocateArray(int initialCapacity) {
    return new byte[initialCapacity];
}
// UnsafeHeapByteBuf通過UnsafeByteBufUtil獲取字節(jié)
static byte getByte(byte[] data, int index) {
    return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index);
}
// HeapByteBuf獲取字節(jié)
static byte getByte(byte[] memory, int index) {
    return memory[index];
}

newDirectBuffer

protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
    return PlatformDependent.hasUnsafe() ?
            new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}

DirectByteBuf構(gòu)造方法大致與heap的類似,只是保存數(shù)據(jù)的容器由字節(jié)數(shù)組變?yōu)榱薺dk的ByteBuffer萝究。相應(yīng)的免都,分配與釋放內(nèi)存的方法也變成調(diào)用jdk的ByteBuffer方法。而UnsafeByteBuf更是直接用long類型記錄內(nèi)存地址帆竹。

// DirectByteBuf獲取字節(jié)
protected byte _getByte(int index) {
    return buffer.get(index);
}
// UnsafeDirectByteBuf獲取字節(jié)
protected byte _getByte(int index) {
    return UnsafeByteBufUtil.getByte(addr(index));
}
// 獲取內(nèi)存地址
final long addr(int index) {
    return memoryAddress + index;
}
// UnsafeByteBufUtil獲取字節(jié)
static byte getByte(long address) {
    return UNSAFE.getByte(address);
}

由于PooledByteBufAllocate內(nèi)容較為龐大绕娘,放入下一節(jié)講述。
未完待續(xù)···

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栽连,一起剝皮案震驚了整個(gè)濱河市险领,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秒紧,老刑警劉巖绢陌,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異熔恢,居然都是意外死亡脐湾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門叙淌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秤掌,“玉大人,你說我怎么就攤上這事鹰霍∥偶” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵衅谷,是天一觀的道長椒拗。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么蚀苛? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任在验,我火速辦了婚禮,結(jié)果婚禮上堵未,老公的妹妹穿的比我還像新娘腋舌。我一直安慰自己,他們只是感情好渗蟹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布块饺。 她就那樣靜靜地躺著,像睡著了一般雌芽。 火紅的嫁衣襯著肌膚如雪授艰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天世落,我揣著相機(jī)與錄音淮腾,去河邊找鬼。 笑死屉佳,一個(gè)胖子當(dāng)著我的面吹牛谷朝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播武花,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼圆凰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了体箕?” 一聲冷哼從身側(cè)響起专钉,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎干旁,沒想到半個(gè)月后驶沼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡争群,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年回怜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片换薄。...
    茶點(diǎn)故事閱讀 38,654評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡玉雾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轻要,到底是詐尸還是另有隱情复旬,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布冲泥,位于F島的核電站驹碍,受9級特大地震影響壁涎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜志秃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一怔球、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浮还,春花似錦竟坛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洼冻,卻和暖如春崭歧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背碘赖。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工驾荣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人普泡。 一個(gè)月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像审编,于是被迫代替她去往敵國和親撼班。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • 焦點(diǎn)高級一期 洛陽 杜紅平 堅(jiān)持分享第534天 今天中午垒酬,我感覺小女發(fā)燒了砰嘁,一量體溫,38.6度勘究,我就讓她吃了退燒...
    隨喜Prajana閱讀 114評論 0 0
  • 內(nèi)容來源于網(wǎng)絡(luò)矮湘,本人只是在此稍作整理,如有涉及版權(quán)問題口糕,歸小甲魚官方所有缅阳。 練習(xí)題(來自小甲魚官方論壇) 0. 列...
    無罪的壞人閱讀 13,900評論 5 17
  • 《非暴力溝通》之傾聽的力量 每個(gè)人都渴望被傾聽,因?yàn)閮A聽會(huì)產(chǎn)生很神奇的力量景描。 試想如果有人傾聽你十办,不對你評頭論足,...
    葉穎群閱讀 109評論 0 0
  • 實(shí)驗(yàn)室折騰一天超棺,回寢室的路上被四個(gè)小姐姐喊住向族,希望我們可以接受一個(gè)采訪:回答零零后的問題以及想問零零后什么。有人說...
    粒子呀閱讀 355評論 0 0
  • 今日日期:2019年1月5日 累計(jì)天數(shù):23/30 ?親子宣言:成為你的媽媽棠绘,是上帝對我的恩典件相! ?孩子第四個(gè)30...
    何川LX閱讀 316評論 0 0