9.PoolSubpage

PoolSubPage

在PoolChunk分配內(nèi)存時推励,如果normCapacity & subpageOverflowMask = 0联予,也就是申請分配的內(nèi)存大小不到一個Page,則會申請分配SubPage。事實上早在PoolChunk構(gòu)造函數(shù)中就初始化了一個成員變量subpages陵珍,這是一個PoolSubPage的數(shù)組握爷,長度為2^maxOrder扑浸,默認2048聘芜,與Page二叉樹葉子結(jié)點數(shù)量一致。此外在PoolArena里也有2個PoolSubpage數(shù)組趋厉,它們被命名為tinySubpagePools和smallSubpagePools寨闹。
tinySubpage是指subpage大小在16B496B之間的數(shù)組,以16B的數(shù)量遞增君账,數(shù)組長度為512B/16B=32鼻忠;smallSubpage是指subpage大小在512B4MB之間的數(shù)組,以2的指數(shù)遞增杈绸,表示分別分配512KB,1MB,2MB,4MB大小的smallSubpage帖蔓,因此它的長度為4。

鋪墊完之后瞳脓,先看一下PoolSubPage初始化過程塑娇。

PoolSubPage初始化

PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
    this.chunk = chunk;
    this.memoryMapIdx = memoryMapIdx;
    this.runOffset = runOffset;
    this.pageSize = pageSize;
    bitmap = new long[pageSize >>> 10];
    init(head, elemSize);
}
void init(PoolSubpage<T> head, int elemSize) {
    doNotDestroy = true;
    this.elemSize = elemSize;
    if (elemSize != 0) {
        maxNumElems = numAvail = pageSize / elemSize;
        nextAvail = 0;
        bitmapLength = maxNumElems >>> 6;
        if ((maxNumElems & 63) != 0) {
            bitmapLength ++;
        }
        for (int i = 0; i < bitmapLength; i ++) {
            bitmap[i] = 0;
        }
    }
    addToPool(head);
}
private void addToPool(PoolSubpage<T> head) {
    prev = head;
    next = head.next;
    next.prev = this;
    head.next = this;
}

顯然,構(gòu)造函數(shù)可以根據(jù)調(diào)用的方法數(shù)分成3個步驟

  1. 初始化chunk等成員變量劫侧。
  2. 計算最大poolSubpage數(shù)量埋酬。
  3. 添加poolSubpage到PoolArena的雙向鏈表中。
    一個個步驟來講述

初始化chunk等成員變量

先介紹一下這里面涉及到的成員變量烧栋。head是一個特殊的PoolSubpage写妥,位于PoolArena中;chunk即是分配所在的PoolChunk审姓;memoryMap是Chunk中的滿二叉樹珍特;runOffset是此次分配相對于滿二叉樹數(shù)組的偏移量;pageSize是Chunk中的pageSize魔吐,默認8KB扎筒;elemSize是一個規(guī)整化后的結(jié)果莱找,在初次分配時,當(dāng)前page將按這個大小進行切分嗜桌,之后相同大小的PoolSubpage將組成雙向鏈表奥溺。bitmap是一個long類型數(shù)組,netty使用它來描述PoolSubpage雙向鏈表中的內(nèi)存分配情況骨宠,數(shù)組長度為pageSize >>> 10浮定,默認為8,這里需要解釋一下這個長度:由于一個Page默認大小為8KB层亿,而PoolSubpage大小最小為16B桦卒,因此最多可以分配8192 / 16 = 512個poolSubpage。由于long類型數(shù)字有64位棕所,所以最多需要8個long元素組成的數(shù)組來描述分配情況闸盔。

計算最大poolSubpage數(shù)量

maxNumElems表示最多可能擁有的poolSubpage數(shù)量悯辙,numAvial表示當(dāng)前還可分配的poolSubpage數(shù)量琳省。在未使用時,maxNumElems和numAvial相等躲撰,都為pageSize / elemSize针贬,相關(guān)邏輯可以參考上文biamap的描述。
bitmapLength表示bitmap數(shù)組的長度拢蛋,因為PoolSubpage并不都只有16B桦他,大一些的PoolSubpage不需要完整的8個long來表示。這個值通過將PoolSubpage除以代表long類型長度的64得到谆棱。如果maxNumElems & 63 != 0快压,表明maxNumElems不是64的整數(shù)倍,需要為這個余數(shù)單獨增加一個bitmap元素垃瞧。之后將所有bitmap元素都初始化為0.

添加poolSubpage到PoolArena的雙向鏈表中

PoolArena的2個PoolSubpage數(shù)組默認都會被設(shè)置為head蔫劣,head并不承載數(shù)據(jù),只作為一個哨兵節(jié)點存在个从。這里添加到PoolArena就是一個簡單的雙向鏈表操作脉幢,只需要注意每次插入PoolSubpage都是插入在head之后,也即頭插法嗦锐。在這之后PoolArena分配相同大小的內(nèi)存時嫌松,可以直接進行分配,而不必先初始化PoolChunk奕污,分配Page后再分配PoolSubpage萎羔。

PoolSubpage分配內(nèi)存

在介紹完P(guān)oolSubpage的初始化和一些成員變量后,開始分析它的內(nèi)存分配過程碳默。

private long allocateSubpage(int normCapacity) {
    PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
    int d = maxOrder;
    synchronized (head) {
        int id = allocateNode(d);
        if (id < 0) {
            return id;
        }
        final PoolSubpage<T>[] subpages = this.subpages;
        final int pageSize = this.pageSize;
        freeBytes -= pageSize;
        int subpageIdx = subpageIdx(id);
        PoolSubpage<T> subpage = subpages[subpageIdx];
        if (subpage == null) {
            subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
            subpages[subpageIdx] = subpage;
        } else {
            subpage.init(head, normCapacity);
        }
        return subpage.allocate();
    }
}

分配過程可以分為5個步驟

  1. 根據(jù)分配的大小找到PoolArena中對應(yīng)的PoolSubpage數(shù)組
  2. 找到可分配的Page節(jié)點外驱,并在可用空間中減去Page節(jié)點的容量育灸。這一步驟與PoolChunk尋找Page節(jié)點完全一樣,不再贅述昵宇。
  3. 找到在PoolChunk中subpage對應(yīng)的索引值磅崭。
  4. 初始化subpage。
  5. 分配subpage級別的內(nèi)存瓦哎。

找到對應(yīng)PoolSubpage數(shù)組

前文說過砸喻,PoolArena中有2個PoolSubpage數(shù)組,數(shù)組中的每一個元素實際上都會形成一個雙向鏈表蒋譬,而鏈表頭結(jié)點就是head割岛。這里倒是有那么一點hashmap的樣子,但hashmap是通過對key取hash值再對數(shù)組長度取模來定位到數(shù)組下標犯助,而PoolSubpage則是根據(jù)將要分配的大小癣漆。

PoolSubpage<T> findSubpagePoolHead(int elemSize) {
    int tableIdx;
    PoolSubpage<T>[] table;
    if (isTiny(elemSize)) { // < 512
        tableIdx = elemSize >>> 4;
        table = tinySubpagePools;
    } else {
        tableIdx = 0;
        elemSize >>>= 10;
        while (elemSize != 0) {
            elemSize >>>= 1;
            tableIdx ++;
        }
        table = smallSubpagePools;
    }

    return table[tableIdx];
}

當(dāng)elemSize小于512時,PoolArena將會去tinyPoolSubpage數(shù)組查找剂买,由于每個PoolSubpage以16B的容量遞增惠爽,所以這里將elemSize右移4即除以16來計算數(shù)組下標。同理當(dāng)elemSize大于等于512時瞬哼,先右移10婚肆,若elemSize等于0,則說明elemSize等于512坐慰,定位到0號元素较性,否則逐步右移,意味著elemSize實際容量比預(yù)期要翻倍结胀。定位到數(shù)組元素后赞咙,返回head節(jié)點。
在步驟2開始前糟港,對head節(jié)點加了互斥鎖攀操,

找到subpage對應(yīng)的索引值

這里就是利用上面查找到的Page節(jié)點在Page二叉樹中的下標值menoryMapIdx與maxSubpageAllocs做異或運算,maxSubpageAllocs在數(shù)值上與Page二叉樹葉子節(jié)點數(shù)相同着逐。由于maxSubpageAllocs是2的冪次方崔赌,默認2048,轉(zhuǎn)化為二進制是1000,0000,0000耸别。而memoryMapIdx也是從2048開始到4095健芭,轉(zhuǎn)化為二進制是從1000,0000,0000到1111,1111,1111,兩者做異或運算時由于首位都為1秀姐,則結(jié)果范圍恰好一一在0-2047內(nèi)慈迈,沒有沖突。精妙的位運算運用。

private int subpageIdx(int memoryMapIdx) {
    return memoryMapIdx ^ maxSubpageAllocs;
}

初始化subpage

在構(gòu)造函數(shù)中有一個參數(shù)runOffset痒留,在前文帶過了谴麦,這里再深入一下它是如何計算的。

private int runOffset(int id) {
    int shift = id ^ 1 << depth(id)
    return shift * runLength(id);
}

private int runLength(int id) {
    return 1 << log2ChunkSize - depth(id);
}

depth(id)就是從PoolChunk中那個不變深度的滿二叉樹depthMap中取得對應(yīng)下標的深度伸头。這里還需要注意順序匾效,是先計算右移再異或,因此這里也是計算這個節(jié)點相對于完全二叉樹每一層最左邊的偏移量恤磷。而runLength(id)則是計算id這一層的每單位偏移量為多少面哼。兩者相乘,就是當(dāng)前相當(dāng)于id所在層最左側(cè)節(jié)點需要偏移多少量扫步。當(dāng)然由于這個方法只在PoolSubpage分配內(nèi)存時調(diào)用魔策,也可以當(dāng)做相對于2048葉子節(jié)點的偏移量。其余初始化流程不贅述河胎。

分配subpage級別的內(nèi)存

long allocate() {
if (elemSize == 0) {
return toHandle(0);
}
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
bitmap[q] |= 1L << r;
if (-- numAvail == 0) {
removeFromPool();
}
return toHandle(bitmapIdx);
}

老規(guī)矩闯袒,忽略檢驗性代碼,分解成4個步驟

  1. 找出PoolSubpage中下一個可以分配的內(nèi)存塊游岳。
  2. 將內(nèi)存塊的bitmapIdx標識為已分配政敢。
  3. 如果該PoolSubpage不可分配了,從PoolArena中移除吭历。
  4. 將bitmapIdx放到返回值的高32位中堕仔。

先看如何找到內(nèi)存地址代表的bitmapIdx

private int getNextAvail() {
        int nextAvail = this.nextAvail;
        if (nextAvail >= 0) {
            this.nextAvail = -1;
            return nextAvail;
        }
        return findNextAvail();
    }

在初始化時擂橘,nextAvial是為0的晌区,說明還未申請分配。當(dāng)?shù)谝淮畏峙渫旰笸ㄕ辏琻extAvial變成了-1朗若,說明已被分配,或許這個變量命名為thisAvial會更好理解?而如果已被分配昌罩,則進入findNextAvial方法尋找下一個可用點內(nèi)存塊哭懈。

findNextAvial則是根據(jù)bitmap來尋找可用內(nèi)存塊。"~bits != 0"表示如果該內(nèi)存塊按位翻轉(zhuǎn)后不為0茎用,說明原本存在為0即未分配的節(jié)點遣总,則進入該內(nèi)存塊尋找指定位置。

private int findNextAvail() {
    final long[] bitmap = this.bitmap;
    final int bitmapLength = this.bitmapLength;
    for (int i = 0; i < bitmapLength; i ++) {
        long bits = bitmap[i];
        if (~bits != 0) {
            return findNextAvail0(i, bits);
        }
    }
    return -1;
}

這里只要謹記參數(shù)i是bitmap數(shù)組中第i個元素轨功,bits表示該元素的64位二進制旭斥。因此baseVal含義是把bitmap數(shù)組第i號元素左移6位。循環(huán)的目的是將bits不斷右移古涧,找到第一個不為0的二進制位垂券。當(dāng)找到時,將baseVal與j做或運算羡滑。這里也可以看出為什么baseVal左移6位菇爪,因為j最多只到63算芯,剛好占用6個二進制位,兩者不會有所沖突凳宙。

private int findNextAvail0(int i, long bits) {
    final int maxNumElems = this.maxNumElems;
    final int baseVal = i << 6;
    for (int j = 0; j < 64; j ++) {
        if ((bits & 1) == 0) {
            int val = baseVal | j;
            if (val < maxNumElems) {
                return val;
            } else {
                break;
            }
        }
        bits >>>= 1;
    }
    return -1;
}

找到bitmapIdx后熙揍,進入步驟2:通過將bitmapIdx右移6位,得到原本的i氏涩,與63做&運算诈嘿,得到j(luò),將bitmap[i]原本的值與j做或運算削葱,表示將bitmap數(shù)組第i號元素的第j位內(nèi)存塊標識為已完成分配奖亚。

int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
bitmap[q] |= 1L << r;

分配完一個內(nèi)存塊后,將numAvial自減并順便做了一個判斷析砸,numAvial為0意味著這個PoolSubpage不能再分配內(nèi)存塊了昔字,所以將其從PoolArena中去掉。這里也是一個簡單的雙鏈表操作首繁,不多描述作郭。

private void removeFromPool() {
    prev.next = next;
    next.prev = prev;
    next = null;
    prev = null;
}

最后則是將bitmapIdx放到返回值的高32位中。

private long toHandle(int bitmapIdx) {
    return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}

這里可以結(jié)合PoolChunk的allocateRun方法的返回值一起思考:netty需要將2個返回值handle統(tǒng)一弦疮,所以將handle用long類型表示夹攒,其中l(wèi)ong類型的低32位表示PoolSubpage所屬的Page的下標,高32位中的7-10位表示bitmap數(shù)組中的元素號胁塞,低6位表示bitmap元素即一個long類型的實際分配情況咏尝。

初始化ByteBuf

在PoolChunk的allocate方法中,返回了handle后最終會進行Bytebuf的初始化啸罢,相關(guān)方法如下编检。

void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
    int memoryMapIdx = memoryMapIdx(handle);
    int bitmapIdx = bitmapIdx(handle);
    if (bitmapIdx == 0) {
        byte val = value(memoryMapIdx);
        buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
                reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
    } else {
        initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
    }
}

memoryMapIdx(handle)和bitmapIdx(handle)分別取handle的后32位和前32位,結(jié)合前文說的handle的含義扰才,不難理解這2個方法允懂。
然后進入了bitmapIdx==0的判斷,如果條件成立衩匣,說明需要分配Page級別的Bytebuf蕾总,否則分配Subpage級別的Bytebuf。
兩種分支都是根據(jù)handle計算offset琅捏,最終調(diào)用buf.init方法將傳入的參數(shù)都保存到成員變量中生百。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市午绳,隨后出現(xiàn)的幾起案子置侍,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜡坊,死亡現(xiàn)場離奇詭異杠输,居然都是意外死亡,警方通過查閱死者的電腦和手機秕衙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門蠢甲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人据忘,你說我怎么就攤上這事鹦牛。” “怎么了勇吊?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵曼追,是天一觀的道長。 經(jīng)常有香客問我汉规,道長礼殊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任针史,我火速辦了婚禮晶伦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘啄枕。我一直安慰自己婚陪,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布频祝。 她就那樣靜靜地躺著泌参,像睡著了一般。 火紅的嫁衣襯著肌膚如雪智润。 梳的紋絲不亂的頭發(fā)上掀泳,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天义起,我揣著相機與錄音,去河邊找鬼接奈。 笑死咐柜,一個胖子當(dāng)著我的面吹牛兼蜈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拙友,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼为狸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了遗契?” 一聲冷哼從身側(cè)響起辐棒,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后漾根,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泰涂,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年辐怕,在試婚紗的時候發(fā)現(xiàn)自己被綠了逼蒙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡寄疏,死狀恐怖是牢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情陕截,我是刑警寧澤驳棱,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站农曲,受9級特大地震影響蹈胡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朋蔫,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一罚渐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧驯妄,春花似錦荷并、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至微猖,卻和暖如春谈息,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凛剥。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工侠仇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人犁珠。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓逻炊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親犁享。 傳聞我的和親對象是個殘疾皇子余素,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

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

  • 但是內(nèi)存拷貝對性能有可能影響比較大,所以Java中可以繞開堆內(nèi)存直接操作堆外內(nèi)存炊昆,問題是創(chuàng)建堆外內(nèi)存的速度比堆內(nèi)存...
    zxRay閱讀 7,374評論 2 32
  • 內(nèi)存管理的主要目的合理分配內(nèi)存桨吊,減少內(nèi)存碎片威根,及時回收資源,提高內(nèi)存的使用效率视乐。從操作系統(tǒng)層面來說医窿,各個軟件在運行...
    史圣杰閱讀 1,186評論 0 1
  • 進程 創(chuàng)建 創(chuàng)建進程用fork()函數(shù)。fork()為子進程創(chuàng)建新的地址空間并且拷貝頁表炊林。子進程的虛擬地址空間...
    梅花怒閱讀 1,907評論 0 7
  • 抓主線姥卢,三個點: 虛擬內(nèi)存組織 虛擬內(nèi)存和物理內(nèi)存的轉(zhuǎn)換 物理內(nèi)存組織 虛擬內(nèi)存組織 平時在進程中,所謂的內(nèi)存地址...
    123archu閱讀 3,199評論 1 7
  • Netty作為一款高性能網(wǎng)絡(luò)應(yīng)用程序框架,實現(xiàn)了一套高性能內(nèi)存管理機制 通過學(xué)習(xí)其中的實現(xiàn)原理奕枝、算法棺榔、并發(fā)設(shè)計,有...
    caison閱讀 1,220評論 2 4