10.netty內(nèi)存管理——PoolArena

PoolArena

在PooledByteBufAllocate初始化最后一步屈暗,就是初始化PoolArena。PoolArena是對內(nèi)存申請和釋放的一個抽象。在類層次上殴玛,PoolArena有2個子類DirectArena和HeapArena,并且將內(nèi)存處理的工作都統(tǒng)一抽象到了PoolArena中。PoolArena主要管理的對象是tinySubpagePools水评、smallSubpagePools和PoolChunkList,前兩者在PoolSubpage一節(jié)已有提及媚送,而PoolChunkList則是一組鏈表對象中燥,并且會根據(jù)PoolChunk內(nèi)存使用率的變化將其中的元素進行轉(zhuǎn)移。

PoolArena成員介紹

通過PoolArena的構(gòu)造函數(shù)看一下常用成員變量塘偎,這里忽略了metric相關(guān)的變量疗涉。

protected PoolArena(PooledByteBufAllocator parent, int pageSize,
          int maxOrder, int pageShifts, int chunkSize, int cacheAlignment) {
        this.parent = parent;
        this.pageSize = pageSize;
        this.maxOrder = maxOrder;
        this.pageShifts = pageShifts;
        this.chunkSize = chunkSize;
        directMemoryCacheAlignment = cacheAlignment;
        directMemoryCacheAlignmentMask = cacheAlignment - 1;
        subpageOverflowMask = ~(pageSize - 1);
        tinySubpagePools = newSubpagePoolArray(numTinySubpagePools);
        for (int i = 0; i < tinySubpagePools.length; i ++) {
            tinySubpagePools[i] = newSubpagePoolHead(pageSize);
        }

        numSmallSubpagePools = pageShifts - 9;
        smallSubpagePools = newSubpagePoolArray(numSmallSubpagePools);
        for (int i = 0; i < smallSubpagePools.length; i ++) {
            smallSubpagePools[i] = newSubpagePoolHead(pageSize);
        }

        q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
        q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
        q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
        q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
        q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
        qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);

        q100.prevList(q075);
        q075.prevList(q050);
        q050.prevList(q025);
        q025.prevList(q000);
        q000.prevList(null);
        qInit.prevList(qInit);

構(gòu)造函數(shù)前半段是一些熟悉的屬性——pageSize、maxOrder等吟秩,忽略掉這些屬性設(shè)置外咱扣,PoolArena主要的成員變量就是tinySubpagePools和smallSubpagePools,這2個在PoolSubpage一小節(jié)已經(jīng)講述過涵防,這里看一下PoolChunkList闹伪。

PoolChunkList

為了理解PoolChunkList,我們先看一下它的構(gòu)造函數(shù)武学。

PoolChunkList實例化

PoolChunkList(PoolArena<T> arena, PoolChunkList<T> nextList, int minUsage, int maxUsage, int chunkSize) {
    this.arena = arena;
    this.nextList = nextList;
    this.minUsage = minUsage;
    this.maxUsage = maxUsage;
    maxCapacity = calculateMaxCapacity(minUsage, chunkSize);
}

private static int calculateMaxCapacity(int minUsage, int chunkSize) {
    minUsage = Math.max(1, minUsage);
    if (minUsage == 100) {
        return 0;
    }
    return  (int) (chunkSize * (100L - minUsage) / 100L);
}

在構(gòu)造函數(shù)中定義了PoolChunkList中元素的內(nèi)存最小祭往、最大使用率,并且根據(jù)最小占用率計算出最大內(nèi)存容量火窒。此外由nextList還可以看出來硼补,PoolChunkList除了自身是一條PoolChunk的鏈表外,不同的PoolChunkList還會形成一個鏈表熏矿。除了上述幾個成員變量外已骇,PoolChunkList還有head和prevList离钝,前者指向PoolChunk的最新加入的節(jié)點,后者是PoolChunkList的前繼節(jié)點褪储。

PoolChunkList添加PoolChunk

接著看一下PoolChunkList添加PoolChunk的add方法卵渴。add方法在3種情況被調(diào)用:

  1. 新創(chuàng)建的Chunk,由一個特殊的PoolChunkList——qInit調(diào)用鲤竹,加入鏈表中浪读。
  2. 在chunk內(nèi)存使用率變化時,需要調(diào)整其所在鏈表時調(diào)用
  3. 調(diào)整鏈表時辛藻,發(fā)現(xiàn)當前PoolChunkList不滿足內(nèi)存使用率碘橘,遞歸調(diào)用。
void add(PoolChunk<T> chunk) {
    if (chunk.usage() >= maxUsage) {
        nextList.add(chunk);
        return;
    }
    add0(chunk);
}

void add0(PoolChunk<T> chunk) {
    chunk.parent = this;
    if (head == null) {
        head = chunk;
        chunk.prev = null;
        chunk.next = null;
    } else {
        chunk.prev = null;
        chunk.next = head;
        head.prev = chunk;
        head = chunk;
    }
}

add方法首先計算傳入的chunk的使用率吱肌,如果使用率超過了當前PoolChunkList的最大使用率痘拆,則遞歸調(diào)用add方法,直到尋找到合適的PoolChunkList后氮墨,調(diào)用add0纺蛆。add0就是流程化的雙向鏈表頭插法添加操作。

PoolChunkList移動PoolChunk

與add相反的是move操作规揪,在free內(nèi)存或者調(diào)整內(nèi)存使用率時桥氏,move0方法被調(diào)用,它會根據(jù)chunk的使用率將其從高使用率的PoolChunkList移動到低使用率的PoolChunkList中粒褒。

private boolean move0(PoolChunk<T> chunk) {
    if (prevList == null) {
        assert chunk.usage() == 0;
        return false;
    }
    return prevList.move(chunk);
}

private boolean move(PoolChunk<T> chunk) {
    if (chunk.usage() < minUsage) {
        return move0(chunk);
    }
    add0(chunk);
    return true;
}

move0這里首先進行判斷识颊,如果當前節(jié)點為0,意味著chunk是從q000鏈表移除奕坟,而q000的最小使用率為1%祥款,所以這里做了一個斷言chunk使用率為0。之后調(diào)用prevList的move方法月杉。
move方法也是先判斷使用率是否小于調(diào)用者最小使用率刃跛,若小于則再次調(diào)用move0方法,否則將chunk添加到調(diào)用該方法的PoolChunkList中苛萎。

PoolChunkList申請內(nèi)存

boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    if (normCapacity > maxCapacity) {
        return false;
    }
    for (PoolChunk<T> cur = head; cur != null; cur = cur.next) {
        if (cur.allocate(buf, reqCapacity, normCapacity)) {
            if (cur.usage() >= maxUsage) {
                remove(cur);
                nextList.add(cur);
            }
            return true;
        }
    }
    return false;
}

首先注意到有2個capacity的入?yún)⒔瓣迹粋€是reqCapacity,表示外部請求申請的內(nèi)存大小腌歉,一個是normCapacity蛙酪,表示經(jīng)過規(guī)整化為2的冪次方的內(nèi)存大小,也是實際申請的內(nèi)存大小翘盖,所以在與PoolChunkList的maxCapacity判斷時用的maxCapacity桂塞。
滿足內(nèi)存大小申請條件后,用當前PoolChunkList內(nèi)部的PoolChunk鏈表一個個嘗試申請馍驯。如果在某個PoolChunk申請內(nèi)存后阁危,發(fā)現(xiàn)其內(nèi)存使用率超過了當前PoolChunkList的最大使用率玛痊,則將其轉(zhuǎn)移到下一個PoolChunkList中去。

PoolChunkList初始化

回到PoolArena中狂打,這里初始化了6個PoolChunkList擂煞,組成了一個q000 <-> q025 <-> q050 <-> q075 <-> q100的雙向鏈表。qInit比較特殊趴乡,他的后繼節(jié)點是q000对省,但前繼節(jié)點是自身。從名字上也可以看出晾捏,qInit負責創(chuàng)建PoolChunk官辽,且在PoolChunk使用率超過qInit的maxUsage(25%)后,將其轉(zhuǎn)移到q000鏈表去粟瞬。因為q000最小使用率就是1%,再小就沒必要留著PoolChunk了萤捆,因此q000的前繼節(jié)點為null裙品。

PoolArena申請內(nèi)存

外界通過調(diào)用allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity)方法來申請內(nèi)存。

PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
    PooledByteBuf<T> buf = newByteBuf(maxCapacity);
    allocate(cache, buf, reqCapacity);
    return buf;
}

這個方法首先調(diào)用了一個抽象方法newByteBuf(int maxCapacity)獲取到PooledByteBuf俗或,然后將申請的內(nèi)存通過的allocate方法放入這個ByteBuf中市怎。

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
final int normCapacity = normalizeCapacity(reqCapacity);
if (isTinyOrSmall(normCapacity)) {
    int tableIdx;
    PoolSubpage<T>[] table;
    boolean tiny = isTiny(normCapacity);
    if (tiny) {
        if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
            return;
        }
        tableIdx = tinyIdx(normCapacity);
        table = tinySubpagePools;
    } else {
        if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
            return;
        }
        tableIdx = smallIdx(normCapacity);
        table = smallSubpagePools;
    }
    final PoolSubpage<T> head = table[tableIdx];

    synchronized (head) {
        final PoolSubpage<T> s = head.next;
        if (s != head) {
            assert s.doNotDestroy && s.elemSize == normCapacity;
            long handle = s.allocate();
            assert handle >= 0;
            s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
            incTinySmallAllocation(tiny);
            return;
        }
    }
        synchronized (this) {
            allocateNormal(buf, reqCapacity, normCapacity);
        }
        incTinySmallAllocation(tiny);
        return;
    }
    if (normCapacity <= chunkSize) {
        if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
            return;
        }
        synchronized (this) {
            allocateNormal(buf, reqCapacity, normCapacity);
            ++allocationsNormal;
        }
    } else {
        allocateHuge(buf, reqCapacity);
    }
}

可以看出這個方法是相當?shù)拈L,但總體邏輯還算清晰辛慰,慣例先分步驟:

  1. 將申請內(nèi)存規(guī)整化区匠。
  2. 申請tiny、small級別的內(nèi)存帅腌。
  3. 申請normal級別的內(nèi)存驰弄。
  4. 申請huge級別的內(nèi)存。

將申請內(nèi)存規(guī)整化速客。

將內(nèi)存大小規(guī)整化這個概念前面多次提到戚篙,他的作用是將內(nèi)存大小向上對齊。對于512B以下大小的內(nèi)存溺职,對齊到大于申請內(nèi)存的下一個16B的倍數(shù)岔擂。比如15B,會對齊到16B浪耘,41B會對齊到48B乱灵。對于大于512B的內(nèi)存,對齊到大于申請內(nèi)存的下一個2的冪次方七冲,比如申請600B痛倚,會對齊到1KB,申請3KB癞埠,會對齊到4KB状原。
它的實現(xiàn)大量使用位運算聋呢,代碼如下。由于directMemoryCacheAlignment默認為0颠区,且通常也不會設(shè)置削锰,這里忽略了。

int normalizeCapacity(int reqCapacity) {
    checkPositiveOrZero(reqCapacity, "reqCapacity");
    if (reqCapacity >= chunkSize) {
        return reqCapacity;
    }
    if (!isTiny(reqCapacity)) {
        int normalizedCapacity = reqCapacity;
        normalizedCapacity --;
        normalizedCapacity |= normalizedCapacity >>>  1;
        normalizedCapacity |= normalizedCapacity >>>  2;
        normalizedCapacity |= normalizedCapacity >>>  4;
        normalizedCapacity |= normalizedCapacity >>>  8;
        normalizedCapacity |= normalizedCapacity >>> 16;
        normalizedCapacity ++;
        if (normalizedCapacity < 0) {
            normalizedCapacity >>>= 1;
        }
        return normalizedCapacity;
    }

    if ((reqCapacity & 15) == 0) {
        return reqCapacity;
    }
    return (reqCapacity & ~15) + 16;
}

首先檢驗了內(nèi)存是否不是負數(shù)毕莱,否則直接拋出異常器贩。之后判斷申請內(nèi)存是否超出了ChunkSize級別姻檀,即16MB芥炭,若超出這個大小,則達到了huge級別氛濒,無需規(guī)整化部服。
若不是tiny級別內(nèi)存唆姐,則先自減,分別與自身無符號右移1廓八、2奉芦、4、8剧蹂、16位做或運算声功,再自增。完成后還做了是否越界的判斷宠叼,越界時先巴,無符號右移一位。
比如600B冒冬,二進制為0010 0101 1000伸蚯,自減后變成0010 0101 0111,做完無符號右移后简烤,變成0011 1111 1111,自增后朝卒,變成0100 0000 0000,即2^10=1024B乐埠。
若為tiny級別抗斤,&15==0表明剛好是16的倍數(shù),直接返回丈咐。否則對15取反后瑞眼,與內(nèi)存做與運算,再加上16棵逊。因為15取反后伤疙,低4位為0,高位全部為1,相當于掩碼徒像。

申請tiny黍特、small級別的內(nèi)存

在對內(nèi)存規(guī)整化以后,首先需要判斷它在哪個范圍锯蛀。對tiny灭衷、small級別來說采用的是 (subpageOverflowMask & normCapacity) == 0 這樣一個判斷條件,其中subpageOverflowMask = ~(pageSize - 1)旁涤,默認低13位為0翔曲,其余高位為1。之后又使用 (normCapacity & 0xFFFFFE00) == 0 進一步劃分tiny和small劈愚。tiny瞳遍、small,包括后續(xù)的norm菌羽,都會先嘗試用緩存分配掠械,如果分配成功則直接返回。緩存分配留待后續(xù)注祖。
在緩存分配失敗后份蝴,會去相應(yīng)的PoolArena中的PoolSubpage數(shù)組定位到對應(yīng)大小PoolSubpage的head結(jié)點。
在分配之前氓轰,先對head加鎖,因為此時PoolChunk.allocateSubpage和PoolChunk.free可能會并發(fā)修改PoolSubpage鏈表浸卦。
加鎖完成進入臨界區(qū)署鸡,迭代head的下一個非空PoolSubpage節(jié)點,調(diào)用其allocate方法進行內(nèi)存分配限嫌。
在分配完成后靴庆,還需要調(diào)用其所在PoolChunk的initBufWithSubpage方法,最終會調(diào)用ByteBuf.init方法進行初始化怒医。分配完成后炉抒,增加對應(yīng)的分配計數(shù)器
若不存在非空的PoolSubpage節(jié)點,則還是需要在normal分配稚叹。

申請normal級別的內(nèi)存

在分配normal級別的內(nèi)存時焰薄,需要對PoolArena對象加鎖,防止其他線程同時分配內(nèi)存扒袖。具體分配的代碼如下

private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
        q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
        q075.allocate(buf, reqCapacity, normCapacity)) {
        return;
    }
    // Add a new chunk.
    PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
    boolean success = c.allocate(buf, reqCapacity, normCapacity);
    assert success;
    qInit.add(c);
}

首先嘗試在5個PoolChunkList中分配內(nèi)存塞茅。PoolChunkList分配內(nèi)存的相關(guān)代碼在前文已經(jīng)涉及。它最終會將分配的PoolChunk放在合適使用率的PoolChunkList中季率。
若PoolChunkList中沒有分配成功(比如在初始狀態(tài)下野瘦,PoolChunkList中除了head節(jié)點沒有其他PoolChunk對象),在會新增一個PoolChunk,調(diào)用新增PoolChunk分配內(nèi)存鞭光。分配完成并且初始化后吏廉,將PoolChunk加入qInit鏈表中,并移動到符合使用率的鏈表中惰许。
分配成功后席覆,會在PoolArena中將對應(yīng)計數(shù)器自增。

申請huge級別內(nèi)存

由于huge級別的內(nèi)存過大啡省,不適合池化管理娜睛,所以申請的是unpooled的內(nèi)存。申請完huge內(nèi)存后卦睹,依然進行初始化和計數(shù)畦戒。

private void allocateHuge(PooledByteBuf<T> buf, int reqCapacity) {
    PoolChunk<T> chunk = newUnpooledChunk(reqCapacity);
    activeBytesHuge.add(chunk.chunkSize());
    buf.initUnpooled(chunk, reqCapacity);
    allocationsHuge.increment();
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市结序,隨后出現(xiàn)的幾起案子障斋,更是在濱河造成了極大的恐慌,老刑警劉巖徐鹤,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垃环,死亡現(xiàn)場離奇詭異,居然都是意外死亡返敬,警方通過查閱死者的電腦和手機遂庄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劲赠,“玉大人涛目,你說我怎么就攤上這事×菖欤” “怎么了霹肝?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長塑煎。 經(jīng)常有香客問我沫换,道長,這世上最難降的妖魔是什么最铁? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任讯赏,我火速辦了婚禮,結(jié)果婚禮上冷尉,老公的妹妹穿的比我還像新娘待逞。我一直安慰自己,他們只是感情好网严,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布识樱。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪怜庸。 梳的紋絲不亂的頭發(fā)上当犯,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機與錄音割疾,去河邊找鬼嚎卫。 笑死,一個胖子當著我的面吹牛宏榕,可吹牛的內(nèi)容都是我干的拓诸。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼麻昼,長吁一口氣:“原來是場噩夢啊……” “哼奠支!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抚芦,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤倍谜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后叉抡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尔崔,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年褥民,在試婚紗的時候發(fā)現(xiàn)自己被綠了季春。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡消返,死狀恐怖载弄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情侦副,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布驼鞭,位于F島的核電站秦驯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏挣棕。R本人自食惡果不足惜译隘,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洛心。 院中可真熱鬧固耘,春花似錦、人聲如沸词身。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至损敷,卻和暖如春葫笼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拗馒。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工路星, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诱桂。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓洋丐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親挥等。 傳聞我的和親對象是個殘疾皇子友绝,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349

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

  • 但是內(nèi)存拷貝對性能有可能影響比較大,所以Java中可以繞開堆內(nèi)存直接操作堆外內(nèi)存触菜,問題是創(chuàng)建堆外內(nèi)存的速度比堆內(nèi)存...
    zxRay閱讀 7,366評論 2 32
  • 內(nèi)存管理的主要目的合理分配內(nèi)存九榔,減少內(nèi)存碎片,及時回收資源涡相,提高內(nèi)存的使用效率哲泊。從操作系統(tǒng)層面來說,各個軟件在運行...
    史圣杰閱讀 1,172評論 0 1
  • Netty作為一款高性能網(wǎng)絡(luò)應(yīng)用程序框架,實現(xiàn)了一套高性能內(nèi)存管理機制 通過學習其中的實現(xiàn)原理丙号、算法先朦、并發(fā)設(shè)計,有...
    caison閱讀 1,210評論 2 4
  • Netty中有一個主要的特點犬缨,就是ByteBuf的實現(xiàn)喳魏, 由于NIO中java.nio.ByteBuf中只有pos...
    橋頭橋尾閱讀 1,107評論 0 2
  • 內(nèi)存分配概述 介紹netty內(nèi)存分配,最為底層怀薛,負責從底層讀據(jù)到ByteBuf刺彩。 三個問題+內(nèi)存類別有哪些+如何減...
    橫渡閱讀 785評論 0 1