Netty源码分析----PoolSubpage

(*文章基于Netty4.1.22版本)

整体介绍

分析PoolChunk的时候,讲到大于等于8KB走Chunk,否则走PoolSubpage,为什么要这么做呢?假如我们需要分配1KB的内存,那么如果还是用Chunk来分配,那么由于Chunk是默认8KB,那么就有7KB的空间浪费了,无法分配,因为一个Chunk是一个整体,所以Netty在Chunk之下拆分了大小相等的内存段,即PoolSubpage,这样空间的利用就更合理了

源码分析

字段介绍

final class PoolSubpage<T> implements PoolSubpageMetric {

    final PoolChunk<T> chunk;
    // 即PoolChunk的id
    private final int memoryMapIdx;
    // page在Chunk叶子节点的相对位移,如果id为2048则runOffset为0,2049则为1,以此类推
    private final int runOffset;
    // Chunk的大小
    private final int pageSize;
    // 用来标记内存段分配情况的数组,原理类似Java的BitSet
    private final long[] bitmap;
    
    // 两个指针
    PoolSubpage<T> prev;
    PoolSubpage<T> next;

    boolean doNotDestroy;
    // 每个内存段的大小
    int elemSize;
    // 内存段的数量
    private int maxNumElems;
    private int bitmapLength;
    private int nextAvail;
    // 可用的内存段数量
    private int numAvail;
}

基本介绍如上,其中需要详细分析的是这个bitmap数组,数组里每个元素是long类型,bitmap在初始化的时候会设置pageSize>>>10个元素,即8个元素

  • 那么为什么是8?

在分配的时候,外部会先经过PoolArena(后续分析),这时会处理请求的字节大小,保证其为2的N次方,且最小为16,那么8KB的情况下,最多会分割成512个内存段,一个long是64位,512/64=8,所以最多只需要8个long元素就可以分配所有段。

每个long元素有64位,每一位可以用来标志一个内存段的使用情况,由于是使用类似BitSet的结构,bitmap的结构图如下:


bitmap结构.png

bitmap有8个long元素,每个long有64位,每一位都能标志一个内存的分配情况,共计8*64=512位。

初始化

    // PoolArena中有个page的pool,head为里面的头结点
    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]; // pageSize / 16 / 64
        init(head, elemSize);
    }
    // 初始化每个段落的基本信息,例如每个内存段大小,内存段的数量,然后加入到Arena的PagePool的头结点中
    void init(PoolSubpage<T> head, int elemSize) {
        doNotDestroy = true;
        this.elemSize = elemSize;
        if (elemSize != 0) {
            // 元素大小=可用数量=Chunk的pageSize / 每个元素的大小
            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);// 加入到头结点后面
    }

分配

在PoolChunk分配的时候,如果小于8KB会调用allocateSubpage,其中会初始化PoolSubpage并用其分配

    private long allocateSubpage(int normCapacity) {
        PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
        synchronized (head) {//
            // ....
            final PoolSubpage<T>[] subpages = this.subpages;
            final int pageSize = this.pageSize;

            freeBytes -= pageSize;
            // subpageIdx = 0 1 2 3 4 ....2047 即最底下的叶子节点个数
            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();
        }
    }
    long allocate() {
        //....
        // 找出一个可用的位置
        final int bitmapIdx = getNextAvail();
        int q = bitmapIdx >>> 6;// 图中的q
        int r = bitmapIdx & 63;// 图中的r
        assert (bitmap[q] >>> r & 1) == 0;
        bitmap[q] |= 1L << r;
        // 没分配一个位置,numAvail-1
        if (-- numAvail == 0) {//当全部分配完毕,将该subpage从链表中移除
            removeFromPool();
        }

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

获取下一个可用的位置

    private int getNextAvail() {
        int nextAvail = this.nextAvail;
        if (nextAvail >= 0) {
            this.nextAvail = -1;
            return nextAvail;
        }
        return findNextAvail()//核心在这里
    }

    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) {// 该long元素上还有未分配的位置,当64位全为1的时候~bits才等于0
                return findNextAvail0(i, bits);
            }
        }
        return -1;
    }

    private int findNextAvail0(int i, long bits) {
        final int maxNumElems = this.maxNumElems;
        final int baseVal = i << 6;//0 64 128....

        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;
    }

getNextAvail会获取0~maxNumElems中可以用的位置

bitmap设值

bitmap[q] |= 1L << r;

这句代码怎么理解呢,其实就是如下的过程:
r=0:0000....0001 ->1
r=1:0000....0010 ->3
r=2:0000....0100 ->7
r=3:0000....1000 ->15
下一个r与上一个bitmap的值做或操作

返回page索引

当使用PoolChunk分配的时候,会返回一个id,即树中节点的值,代表该节点被分配,那么在使用PoolSubpage的时候也需要返回一个值代表page中某个节点被分配,通过上面的分析我们知道了,Page最多有512个节点,那么返回的值有0~511,那么如果就这样返回出去了,那么外部就无法知道到底是分配了Chunk还是Page,因为释放的时候,会传入这个唯一标志值,所以在这里调用了toHandle进行了一波骚操作,会让其变成一个64位的信息,然后释放的时候就可以知道该释放Chunk还是Page。
当然了,如果直到了用Chunk还是Page还是不够的,因为不知道用的是哪个Chunk的Page,所以在toHandle方法中还会看到对memoryMapIdx进行了计算

0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;

左移32位转换成64位信息是为了判断是用Page还是用Chunk,和memoryMapIdx做位运算是为了判断在属于哪个Chunk
(这里要感谢闪电哥的热心解答,嘿嘿)

释放

    // 如果返回true代表该PoolSubpage已经被使用
    // 如果返回false代表该PoolSubpage已经没有使用了 
    boolean free(PoolSubpage<T> head, int bitmapIdx) {
        if (elemSize == 0) {
            return true;
        }
        int q = bitmapIdx >>> 6;
        int r = bitmapIdx & 63;
        assert (bitmap[q] >>> r & 1) != 0;
        bitmap[q] ^= 1L << r;
        // 释放的时候设置nextAvail为当前的bitmapIdx,下次getNextAvail的时候可以直接使用
        setNextAvail(bitmapIdx);

        if (numAvail ++ == 0) {
            addToPool(head);// 加入到头结点后面
            return true;
        }

        if (numAvail != maxNumElems) {
            return true;
        } else {//一个未用
            if (prev == next) {// 如果只有head和当前节点,那么直接返回
                return true;
            }
            // 没有使用了,那么可以把该节点从链表中移除
            doNotDestroy = false;
            removeFromPool();
            return false;
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容

  • (*文章基于Netty4.1.22版本)Netty内存管理这块比较复杂,断断续续看了一个多月了,总要有点输出,写几...
    _六道木阅读 420评论 0 1
  • 在学习jemalloc之前可以了解一下glibc malloc,jemalloc没有'unlinking' 和 '...
    dcharles阅读 6,676评论 0 7
  • 但是内存拷贝对性能有可能影响比较大,所以Java中可以绕开堆内存直接操作堆外内存,问题是创建堆外内存的速度比堆内存...
    zxRay阅读 7,376评论 2 32
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,797评论 0 38
  • 文/梨若 你好!生活! 我想要烤箱、吊椅、投影仪、咖啡器具、落地灯、绿植。 愿望清单。 ...
    梨若阅读 251评论 0 3