Netty源碼_內(nèi)存管理(jemalloc3)

Netty 是一個高性能的網(wǎng)絡(luò)應(yīng)用程序框架习劫,主要就是進行數(shù)據(jù)的交互叙谨,所以必須有一個高效的內(nèi)存分配器许溅。
內(nèi)存分配器的功能就兩個:

  • 用戶申請內(nèi)存時瓤鼻,分配給它內(nèi)存塊。
  • 用戶主動釋放內(nèi)存時贤重,回收這個內(nèi)存塊茬祷。

一般我們的做法是:

  • 先申請一個較大的內(nèi)存塊。
  • 當(dāng)用戶申請內(nèi)存時并蝗,從這個內(nèi)存塊中牲迫,分割符合申請內(nèi)存大小的內(nèi)存塊給用戶。
  • 用戶主動釋放內(nèi)存時借卧,再將這個內(nèi)存塊回收。

但是這么做有個問題筛峭,因為用戶申請內(nèi)存的大小各不相同铐刘,分配的內(nèi)存塊大小就不一樣,回收以后就是各種尺寸的內(nèi)存碎片影晓。

  • 例如镰吵,我們有一個20大小的總內(nèi)存塊,分配給用戶兩個大小為5 內(nèi)存塊挂签,和一個內(nèi)存為 4 內(nèi)存塊疤祭,兩個內(nèi)存為2 內(nèi)存塊;
  • 之后都回收了,就有兩個為 5,一個為4,三個為2內(nèi)存碎片饵婆。
  • 這個時候在申請內(nèi)存為 6 的內(nèi)存塊時勺馆,發(fā)現(xiàn)沒有辦法分配了。

為了解決這個問題侨核,能夠高效地進行內(nèi)存分配草穆,就要使用內(nèi)存分配算法了。

  • Netty 4.1.45版本之前使用的是 jemalloc3 算法來進行內(nèi)存分配的搓译;
  • 而在4.1.45版本之后使用的是 jemalloc4 算法來進行內(nèi)存分配的悲柱。
  • 本篇文章我們先介紹 jemalloc3 算法實現(xiàn)。

一. 劃分內(nèi)存規(guī)格

產(chǎn)生內(nèi)存碎片最主要的原因就是因為用戶申請的內(nèi)存大小不一樣些己。

那么如果用戶申請的內(nèi)存大小都一樣豌鸡,那么不就沒有內(nèi)存碎片了么。

想法雖然是好的段标,但是明顯是不可能的涯冠,因為程序運行過程中,需要的內(nèi)存本來就是不同的怀樟。
那么我們就換一個思路功偿,雖然不能要求申請的內(nèi)存大小都一樣,但是可以提前劃分好不同規(guī)格的內(nèi)存,然后根據(jù)請的內(nèi)存大小不同械荷,分配不同規(guī)格的內(nèi)存快共耍。

jemalloc3_內(nèi)存規(guī)格.png

如上圖所示,jemalloc3 一共將內(nèi)存分為四種類型:

內(nèi)存規(guī)格 描述
Tiny 微小規(guī)格內(nèi)存塊吨瞎,容量從16B496B 一共31 個內(nèi)存規(guī)格痹兜,每個規(guī)格容量相差16B
Small 小規(guī)格內(nèi)存塊,容量從512B4KB 一共4 個內(nèi)存規(guī)格颤诀,每個規(guī)格容量相差一倍
Normal 正常規(guī)格內(nèi)存塊字旭,容量從8KB16MB 一共11 個內(nèi)存規(guī)格,每個規(guī)格容量相差一倍
Huge 巨大內(nèi)存塊崖叫,不會放在內(nèi)存管理中遗淳,直接內(nèi)存中申請

因此就可以根據(jù)用戶申請的內(nèi)存大小,直接對應(yīng)規(guī)格的內(nèi)存塊心傀。

  • 例如申請 40B, 那么就分配 48B 規(guī)格的內(nèi)存塊屈暗,雖然有 8B 的字節(jié)被浪費了,但是避免了內(nèi)存碎片的產(chǎn)生脂男。
  • 你會發(fā)現(xiàn)從Small 開始养叛,每個規(guī)格內(nèi)存塊相差都是一倍,這就可以導(dǎo)致 50% 的內(nèi)存浪費宰翅;例如我們申請 513B 大小弃甥,那么只能分配1KB 規(guī)格的內(nèi)存塊。這個是 jemalloc3 算法的缺陷汁讼,只能使用 jemalloc4 算法進行改進淆攻,以后我們會說到。

二. 內(nèi)存規(guī)格算法實現(xiàn)

內(nèi)存規(guī)格的劃分作用和意義我們已經(jīng)了解了掉缺,那么怎么實現(xiàn)它呢?
Netty 中使用 PoolChunk 來進行內(nèi)存分配:

  • PoolChunk 先申請一大塊內(nèi)存memory(可以是字節(jié)數(shù)組卜录,也可以是DirectByteBuffer),大小就是chunkSize(16MB)眶明。
  • 我們知道 Normal 規(guī)格最小內(nèi)存塊是 pageSize(8KB) 容量艰毒,那么就要能記錄最小 Normal 規(guī)格內(nèi)存塊使用情況。
  • TinySmall 規(guī)格內(nèi)存塊小于 pageSize 大小搜囱,可以使用一個最小 Normal 規(guī)格內(nèi)存塊來分配多個 TinySmall 規(guī)格內(nèi)存塊丑瞧。
內(nèi)存規(guī)格算法實現(xiàn).png

如圖所示:

  • PoolChunk 使用一個滿二叉樹(用數(shù)組實現(xiàn))來記錄內(nèi)存塊的分配使用情況。

    • 因為chunkSize == 16MB蜀肘,且 pageSize == 8KB绊汹,那么樹的深度depth 一共 12 層(從011)。
    • 根據(jù)不同深度扮宠,就可以獲得不同大小的內(nèi)存塊西乖,例如最底層即11層所有節(jié)點對應(yīng)的內(nèi)存塊大小就是8KB
  • 使用數(shù)組來實現(xiàn)這個滿二叉樹。

    • 這里有兩個數(shù)組 memoryMapdepthMap获雕,大小都是4096薄腻。做了特殊處理,下標0 這個位置沒有任何意義届案,從下標 1 開始庵楷。
    • depthMap 的值表示當(dāng)前下標對應(yīng)在二叉樹中的層數(shù)。例如下標為1的值是 0,表示第 0 層;下標為 6 的值是 2,表示第 2 層;下標為 2048 的值是 11,表示第 11 層楣颠。
    • memoryMap 的值表示當(dāng)前這個節(jié)點能分配的內(nèi)存塊大小尽纽。剛開始時和depthMap 的值是一樣的,但是當(dāng)它的子節(jié)點被分配了童漩,那么值就會變弄贿。例如剛開始時,下標為 4 的值是 2矫膨,表示能分配 4MB 內(nèi)存塊大锌娲骸;如果它的一個子節(jié)點被分配了豆拨,那么它的值就會變成 3,表示只能分配 2MB 內(nèi)存塊大小能庆。
  • 使用 bitmap 數(shù)據(jù)記錄TinySmall規(guī)格內(nèi)存使用情況

    • 最底層的內(nèi)存塊可以在分成 TinySmall規(guī)格小內(nèi)存塊施禾。
    • 一旦在最底層的內(nèi)存塊分配了一個 TinySmall規(guī)格小內(nèi)存塊,那么這個最底層的內(nèi)存塊就表示被使用了搁胆,而且這個內(nèi)存塊只能分配剛分配那個大小的規(guī)格的小內(nèi)存塊弥搞,直到它被回收(即由它分配的小內(nèi)存快都被釋放),進行重新分配渠旁,那么可以分配其他大小的規(guī)格的小內(nèi)存塊攀例。即由第一次分配的規(guī)格大小來決定。
    • 通過bitmap 位圖數(shù)組來記錄顾腊,已經(jīng)在最底層的內(nèi)存塊上分配了那些小內(nèi)存塊粤铭。因為最小內(nèi)存塊大小是16B,而最底層的內(nèi)存塊大小是8KB,因此最多可以分512塊杂靶;一個 long 類型有64 位二進制數(shù)梆惯,所以最多需要8long 類型就可以記錄。
    • 通過 bitmapIdx 的值吗垮,可以得到在bitmap 位圖數(shù)組中的那一個long 類型的那一位垛吗。通過 bitmapIdx >>> 6 (即除以64) 得到bitmap 位圖數(shù)組的下標;通過 bitmapIdx & 63(即整除64 的余數(shù))得到占據(jù)long 類型那一位烁登。
  • 通過 handle 來記錄偏移量和內(nèi)存塊大小

    • 32 位用來記錄 bitmapIdx怯屉,從前面介紹 bitmapIdx的值很小的,最大值就是 64 * 8。最高位肯定是0锨络,次高位(0x4000000000000000L)其實是用來記錄是不是TinySmall類型規(guī)格赌躺。
    • 32 位用來記錄 memoryMapIdx
    • 如果是 Normal規(guī)格足删,高32 位的值肯定是0寿谴;通過memoryMapIdxdepthMap數(shù)組獲取對應(yīng)層數(shù),這樣就能得到內(nèi)存塊大小了失受;根據(jù) memoryMapIdx 可以計算在當(dāng)前這一層的偏移值讶泰。例如 memoryMapIdx = 2050,那么是第11 層拂到,大小就是8KB痪署;偏移值就是 2050 - 2048 = 2,那么偏移量就是 16KB兄旬;因此我們就在偏移量16KB處分割一塊8KB大小的內(nèi)存塊給用戶使用狼犯。
    • 如果是TinySmall規(guī)格,那么肯定是在最底層领铐,先通過memoryMapIdx 計算偏移值悯森,得到偏移量,然后得到這個最底層內(nèi)存塊分割成小內(nèi)存的大小绪撵,再根據(jù)bitmapIdx值得到在這個最底層內(nèi)存塊上的偏移量瓢姻,最后就能得到最終偏移量和分割內(nèi)存塊大小了。

三. 源碼實現(xiàn)

3.1 PoolSubpage

3.1.1 初始化

    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;
        // 因為 long 類型是8個字節(jié)音诈,64位二進制數(shù)幻碱;
        // 而 Tiny 類型最小容量都是 16 個字節(jié)。
        // 所以 bitmap 位圖數(shù)組最大長度就是  pageSize / 16/ 64
        bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
        init(head, elemSize);
    }

    void init(PoolSubpage<T> head, int elemSize) {
        doNotDestroy = true;
        // 當(dāng)前這 PoolSubpage 只會分配 elemSize 大小容量的內(nèi)存
        this.elemSize = elemSize;
        if (elemSize != 0) {
            // PoolSubpage 一共可以分配多少塊這個容量的內(nèi)存
            maxNumElems = numAvail = pageSize / elemSize;
            nextAvail = 0;
            // 無符號右移6位细溅,也就是除以64褥傍,因為一個 long 有64個二進制位
            bitmapLength = maxNumElems >>> 6;
            // 如果 maxNumElems 不能整除 64,那么就要將 bitmapLength 加一
            if ((maxNumElems & 63) != 0) {
                bitmapLength ++;
            }

            for (int i = 0; i < bitmapLength; i ++) {
                bitmap[i] = 0;
            }
        }
        // 添加到 PoolArena 中對應(yīng)尺寸容量的PoolSubpage鏈表中
        addToPool(head);
    }
  • 剛開始創(chuàng)建的時候喇聊,主要是創(chuàng)建 bitmap 位圖數(shù)組恍风,數(shù)組長度就是 pageSize >>> 10,即除以64位二進制數(shù)誓篱,和最小Tiny類型規(guī)格都是16 個字節(jié)邻耕。
  • init(...) 初始化方法,剛創(chuàng)建的時候或者PoolSubpage被回收重新使用的時候調(diào)用燕鸽。
  • 確定當(dāng)前PoolSubpage分配內(nèi)存塊大小elemSize兄世;
  • 計算最多分配多少這個大小的內(nèi)存塊。
  • 計算真實 bitmap位圖數(shù)組長度bitmapLength啊研。
  • 將這個PoolSubpage添加到PoolArena中對應(yīng)尺寸容量的PoolSubpage鏈表中御滩,這樣就不需要需要查找鸥拧,加快內(nèi)存塊分配速度。

3.1.2 分配內(nèi)存塊

    /**
     * 返回子頁面內(nèi)存分配的位圖索引
     * 使用 long 類型每個二進制位數(shù)`0`或 `1` 來記錄這塊內(nèi)存有沒有被分配過削解,
     * 因為 long 是8個字節(jié)富弦,64位二進制數(shù),所以可以表示 64 個內(nèi)存塊分配情況氛驮。
     */
    long allocate() {
        if (elemSize == 0) {
            return toHandle(0);
        }

        if (numAvail == 0 || !doNotDestroy) {
            return -1;
        }

        // 得到下一個可用的位圖 bitmap 索引
        final int bitmapIdx = getNextAvail();
        // 除以 64 得到的整數(shù)腕柜,即 bitmap[] 數(shù)組的下標
        int q = bitmapIdx >>> 6;
        // 與 64 的余數(shù),即占據(jù)的 long 類型的位數(shù)
        int r = bitmapIdx & 63;
        // 必須是 0矫废, 表示這一塊內(nèi)存沒有被分配
        assert (bitmap[q] >>> r & 1) == 0;
        // 將r對應(yīng)二進制位設(shè)置為1盏缤,表示這一位代表的內(nèi)存塊已經(jīng)被分配了
        bitmap[q] |= 1L << r;

        if (-- numAvail == 0) {
            // 如果可分配內(nèi)存塊的數(shù)量numAvail為0,
            // 那么就要這個 PoolSubpage 從 PoolArena 中
            // 對應(yīng)尺寸容量的PoolSubpage鏈表中移除蓖扑。
            removeFromPool();
        }

        // 使用 long類型的高32位儲存 bitmapIdx 的值唉铜,即使用 PoolSubpage 中那一塊的內(nèi)存;
        // 低32位儲存 memoryMapIdx 的值律杠,即表示使用那一個 PoolSubpage
        return toHandle(bitmapIdx);
    }

    private long toHandle(int bitmapIdx) {
        // 雖然我們使用高 32 為表示 bitmapIdx潭流,但是當(dāng)bitmapIdx = 0 時,
        // 就無法確定是否表示 bitmapIdx 的值柜去。
        // 所以這里就 0x4000000000000000L | (long) bitmapIdx << 32灰嫉,那進行區(qū)分。
        // 放心  bitmapIdx << 32 是不可能超過 0x4000000000000000L
        return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
    }

方法流程:

  • 如果沒有可用內(nèi)存塊了嗓奢,就直接返回 -1熬甫。
  • 通過 getNextAvail() 方法,獲取下一個能分配的內(nèi)存塊的位圖索引bitmapIdx蔓罚。
  • 根據(jù)位圖索引bitmapIdxbitmap 位圖數(shù)組中對應(yīng)二進制位設(shè)置為1,表示已經(jīng)被分配了瞻颂。
  • 如果分配之后沒有內(nèi)存塊了豺谈,就將這個PoolSubpagePoolArena中對應(yīng)尺寸容量的PoolSubpage鏈表中刪除,因為已經(jīng)不能分配了贡这。
  • 通過 toHandle(bitmapIdx) 返回 handle 值茬末。

3.1.3 回收內(nèi)存塊

 /**
     * 返回 true,表示這個 PoolSubpage 還在使用盖矫,即上面還有其他小內(nèi)存塊被使用丽惭;
     * 返回 false,表示這個 PoolSubpage 上面分配的小內(nèi)存塊都釋放了辈双,可以回收整個 PoolSubpage责掏。
     */
    boolean free(PoolSubpage<T> head, int bitmapIdx) {
        if (elemSize == 0) {
            return true;
        }
        // 得到位圖 bitmap 中的下標
        int q = bitmapIdx >>> 6;
        // 得到使用 long 類型中那一位
        int r = bitmapIdx & 63;
        // 必須不能是 0, 表示這個 bitmapIdx 對應(yīng)內(nèi)存塊肯定是在被使用
        assert (bitmap[q] >>> r & 1) != 0;
        // 將r對應(yīng)二進制位設(shè)置為0湃望,表示這一位代表的內(nèi)存塊已經(jīng)被釋放了
        bitmap[q] ^= 1L << r;

        // 將 bitmapIdx 設(shè)置為下一個可以使用的內(nèi)存塊索引换衬,
        // 因為剛被釋放痰驱,這樣就不用進行搜索來查找可用內(nèi)存塊索引。
        setNextAvail(bitmapIdx);

        if (numAvail ++ == 0) {
            // 如果可分配內(nèi)存塊的數(shù)量numAvail從0開始增加瞳浦,
            // 那么就要重新添加到 PoolArena 中對應(yīng)尺寸容量的PoolSubpage鏈表中
            addToPool(head);
            return true;
        }

        if (numAvail != maxNumElems) {
            return true;
        } else {
            // 子頁面未使用(numAvail == maxNumElems)
            if (prev == next) {
                // 如果 prev == next担映,即 subpage 組成的鏈表中沒有其他 subpage,不能刪除它
                return true;
            }

            // 如果 prev != next叫潦,即 subpage 組成的鏈表中還有其他 subpage蝇完,那么就刪除它
            doNotDestroy = false;
            removeFromPool();
            return false;
        }
    }
  • 根據(jù)位圖索引bitmapIdxbitmap 位圖數(shù)組中對應(yīng)二進制位設(shè)置為0,表示已經(jīng)被釋放了矗蕊。
  • 調(diào)用 setNextAvail(bitmapIdx)短蜕,加快下一次分配內(nèi)存塊的速度,不需要重新查找了拔妥。
  • 最后再處理一下 PoolArena中對應(yīng)尺寸容量的PoolSubpage鏈表忿危。

3.2 PoolChunk

3.2.1 分配內(nèi)存塊

3.2.1.1 allocate 方法

boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
        final long handle;
        if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
            //  >= pageSize,即 Normal 規(guī)格類型內(nèi)存塊没龙,通過 allocateRun 方法分配
            handle =  allocateRun(normCapacity);
        } else {
            // 分配 Tiny 和 Small 規(guī)格類型內(nèi)存塊
            handle = allocateSubpage(normCapacity);
        }

        if (handle < 0) {
            // 小于 0若厚, 說明當(dāng)前PoolChunk都被分配完了
            return false;
        }
        ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
        // 使用 handle 來初始化 池化緩存區(qū)PooledByteBuf
        initBuf(buf, nioBuffer, handle, reqCapacity);
        return true;
    }
  • 通過 allocateRun(...) 方法,分配 Normal 規(guī)格類型內(nèi)存塊;通過 allocateSubpage(normCapacity)方法民轴,分配TinySmall 規(guī)格類型內(nèi)存塊咱士。
  • 通過 initBuf(...) 方法,使用申請的內(nèi)存塊handle 來初始化池化緩存區(qū)PooledByteBuf筝家。

3.2.1.2 allocateRun 方法

    private long allocateRun(int normCapacity) {
        /**
         * 默認情況下洼裤,maxOrder=11,pageShifts=13
         * normCapacity肯定是大于或者等于pageSize溪王,即 log2(normCapacity) >= pageShifts
         *
         * d 表示在第幾層可以分配這個尺寸容量normCapacity 的內(nèi)存塊腮鞍。
         * 最底層,即11層莹菱,最多只能分配 pageSize尺寸容量的內(nèi)存塊移国。
         */
        int d = maxOrder - (log2(normCapacity) - pageShifts);
        /**
         * 得到尺寸容量normCapacity 內(nèi)存塊的索引id
         */
        int id = allocateNode(d);
        if (id < 0) {
            // 小于 0, 說明當(dāng)前PoolChunk都被分配完了
            return id;
        }
        freeBytes -= runLength(id);
        return id;
    }
  • 通過 allocateNode(d) 方法獲取 memoryMapIdx 的值道伟。
  • 減少當(dāng)前 PoolChunk 可用內(nèi)存字節(jié)數(shù)freeBytes迹缀。

3.2.1.3 allocateNode 方法

     private int allocateNode(int d) {
        // d 代表層數(shù),其實也代表需要的內(nèi)存容量蜜徽,(1 << (maxOrder - d)) * pageSize
        // 當(dāng) d 和 maxOrder 相等祝懂,即需要內(nèi)存容量就是 pageSize
        int id = 1;
        /**
         * 例如 d = 11
         * 那么 1 << d   就是 100000000000
         *  - (1 << d)  就是 1111111111111111111111111111111111111111111111111111100000000000
         *  所以 initial的作用就是用來快速判斷某個值是不是小于 (1 << d)
         */
        int initial = - (1 << d); // has last d bits = 0 and rest all = 1
        byte val = value(id);
        if (val > d) { // unusable
            return -1;
        }
        /**
         * val < d 表示當(dāng)前節(jié)點id對應(yīng)的內(nèi)存容量還大于 d 對應(yīng)的內(nèi)存容量,繼續(xù)尋找拘鞋。
         * (id & initial) == 0 只有當(dāng) id < (1 << d) 時成立砚蓬,保證 id 是 d層的。
         */
        while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
            id <<= 1;
            val = value(id);
            if (val > d) {
                /**
                 * val > d盆色,表示從 id 節(jié)點對應(yīng)的內(nèi)存容量已經(jīng)不足要求內(nèi)存塊的大小了怜械,
                 * 但是它能走到這一個判斷颅和,說明 id 節(jié)點的父節(jié)點對應(yīng)的內(nèi)存容量是可以滿足內(nèi)存塊的大小的;
                 * 那一定是因為 id 節(jié)點兄弟節(jié)點對應(yīng)內(nèi)存容量能滿足內(nèi)存塊的大小缕允。
                 *
                 * id ^= 1 就是得到 id 節(jié)點的兄弟節(jié)點
                 */
                id ^= 1;
                val = value(id);
            }
        }
        byte value = value(id);
        assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
                value, id & initial, d);
        // 將當(dāng)前 memoryMap 的下標id 的值設(shè)置為 unusable峡扩,
        // 表示它已經(jīng)被使用了, unusable = maxOrder + 1
        setValue(id, unusable); // mark as unusable
        // 更新這個 id 之上所有父節(jié)點的值障本,
        // 因為id 節(jié)點被使用了教届,那么它之上所有父節(jié)點代表的內(nèi)存容量都收到影響。
        updateParentsAlloc(id);
        return id;
    }

    private void updateParentsAlloc(int id) {
        // 通過循環(huán)更新所有父節(jié)點的值驾霜。
        while (id > 1) {
            int parentId = id >>> 1;
            byte val1 = value(id);
            byte val2 = value(id ^ 1);
            // 尋找父節(jié)點對應(yīng)子節(jié)點中較小的值
            byte val = val1 < val2 ? val1 : val2;
            setValue(parentId, val);
            id = parentId;
        }
    }
  • 現(xiàn)在滿二叉樹中案训,在d 對應(yīng)的那層中尋找還沒有被分配的節(jié)點(從左到右尋找),返回這個節(jié)點的下標值(即memoryMapIdx)粪糙。
  • 通過 setValue(id, unusable) 方法强霎,將這個節(jié)點值設(shè)置成unusable,表示這個節(jié)點已經(jīng)被分配了。
  • 通過 updateParentsAlloc(id) 方法更新父節(jié)點可分配的內(nèi)存大小蓉冈。
  • 因為 d 層有個節(jié)點被分配了城舞,那么這個節(jié)點的父節(jié)點以及父節(jié)點的父節(jié)點等,它們的可分配的內(nèi)存大小就和它們未分配的那個子節(jié)點大小一樣了寞酿。

3.2.1.4 allocateNode 方法

    private long allocateSubpage(int normCapacity) {
        // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
        // This is need as we may add it back and so alter the linked-list structure.
        // 得到 PoolArena 中對應(yīng)尺寸容量的PoolSubpage鏈表頭
        PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
        // 因為 Tiny 或者 Small類型容量都小于 pageSize,
        // 所以它們肯定只使用最低層一個 PoolSubpage
        int d = maxOrder;
        synchronized (head) {
            // 尋找沒有被分配的 PoolSubpage 索引id
            int id = allocateNode(d);
            if (id < 0) {
                // 小于 0家夺, 說明當(dāng)前PoolChunk都被分配完了
                return id;
            }

            final PoolSubpage<T>[] subpages = this.subpages;
            final int pageSize = this.pageSize;

            // 當(dāng)前PoolChunk 可用字節(jié)數(shù)
            freeBytes -= pageSize;

            // 得到 this.subpages 的下標
            int subpageIdx = subpageIdx(id);
            // 需要容量normCapacity的內(nèi)存就從這個 subpage 中分配
            // 分配之后,這個 subpage 就只能存放這個尺寸容量normCapacity的內(nèi)存
            // 這里的 subpage 肯定是沒有分配過內(nèi)存的伐弹,
            // 因為通過 allocateNode(d) 找到的肯定是沒有分配過內(nèi)存的拉馋,
            PoolSubpage<T> subpage = subpages[subpageIdx];
            if (subpage == null) {
                // 創(chuàng)建 PoolSubpage 實例,構(gòu)造方法中會調(diào)用 subpage.init(head, normCapacity) 方法
                subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
                subpages[subpageIdx] = subpage;
            } else {
                // 這個 PoolSubpage 之前被用過惨好,但是被釋放了煌茴。
                // 初始化, 將這個 PoolSubpage 能分配的容量尺寸就是 normCapacity
                // 再將這個 PoolSubpage 添加到 PoolArena 對應(yīng)容量尺寸 PoolSubpage<T> 數(shù)組中
                // 這樣下次再請求這個尺寸的內(nèi)存時,直接從 PoolSubpage<T> 數(shù)組中找到這個 PoolSubpage日川,
                // 進行內(nèi)存分配
                subpage.init(head, normCapacity);
            }
            // 在PoolSubpage上進行內(nèi)存分配
            return subpage.allocate();
        }
    }
  • 通過 allocateNode(d) 方法在最底層尋找未分配的內(nèi)存塊蔓腐。
  • 減少當(dāng)前 PoolChunk 可用內(nèi)存字節(jié)數(shù) freeBytes
  • 初始化一個 PoolSubpage, 通過它的subpage.allocate() 方法進行TinySmall 規(guī)格類型內(nèi)存塊分配逗鸣。

3.2.1.5 initBuf 方法

    void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
        // 因為 handle 高32位表示 bitmapIdx, 低32位表示 memoryMapIdx
        int memoryMapIdx = memoryMapIdx(handle);
        int bitmapIdx = bitmapIdx(handle);
        // 因為 0x4000000000000000L | (long) bitmapIdx << 32绰精,
        // 所以 bitmapIdx == 0時撒璧,一定是 Normal 類型
        if (bitmapIdx == 0) {
            byte val = value(memoryMapIdx);
            // 肯定是被使用狀態(tài)
            assert val == unusable : String.valueOf(val);
            // runOffset(memoryMapIdx) 表示在當(dāng)前這個 PoolChunk 的字節(jié)偏移量
            // runLength(memoryMapIdx) 這個內(nèi)存塊容量
            buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
                    reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
        } else {

            initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
        }
    }

    private int runOffset(int id) {
        // depth(id) 得到id對應(yīng)層數(shù)
        // id ^ 1 << depth(id) 就是這個id節(jié)點在這一層的偏移值
        // 例如 id = 2049,depth(id) 就是 11笨使,1 << depth(id) 就是 2048
        // 2049 ^ 2048 = 1
        int shift = id ^ 1 << depth(id);
        // 偏移量shift乘以 id對應(yīng)內(nèi)存塊容量runLength(id)卿樱,
        // 就得到最后的字節(jié)偏移量
        return shift * runLength(id);
    }

    private int runLength(int id) {
        // represents the size in #bytes supported by node 'id' in the tree
        // 節(jié)點id對應(yīng)的內(nèi)存塊大小,單位是字節(jié)硫椰,一個字節(jié)就是8位bits
        // log2ChunkSize 是 chunkSize 的log2 的對數(shù)繁调,
        // 而 chunkSize = pageSize * maxOrder, depth(id) 就是求id節(jié)點對應(yīng)的層數(shù)萨蚕,最低層就是maxOrder,
        // 所以id節(jié)點在最底層蹄胰,那么depth(id)就是maxOrder岳遥,那么結(jié)果值就是 pageSize。
        return 1 << log2ChunkSize - depth(id);
    }

初始化 Normal 規(guī)格的 PooledByteBuf, 通過 runOffset(memoryMapIdx) 方法計算偏移量裕寨,通過 runLength(memoryMapIdx) 方法計算內(nèi)存塊大小浩蓉。

3.2.1.6 initBuf 方法

    private void initBufWithSubpage(PooledByteBuf<T> buf, ByteBuffer nioBuffer,
                                    long handle, int bitmapIdx, int reqCapacity) {
        assert bitmapIdx != 0;

        int memoryMapIdx = memoryMapIdx(handle);

        // 通過 memoryMapIdx 找到 PoolSubpage
        PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
        assert subpage.doNotDestroy;
        // 容量必須小于或等于 PoolSubpage 對應(yīng)的塊容量elemSize
        assert reqCapacity <= subpage.elemSize;

        // runOffset(memoryMapIdx) 表示這個 PoolSubpage 在當(dāng)前這個 PoolChunk 的字節(jié)偏移量;
        // (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize
        // 就是表示這個 Tiny或者Small類型內(nèi)存塊在 中PoolSubpage 偏移量宾袜。
        buf.init(
            this, nioBuffer, handle,
            runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset,
                reqCapacity, subpage.elemSize, arena.parent.threadCache());
    }

初始化 TinySmall 規(guī)格類型的 PooledByteBuf, 內(nèi)存塊大小通過 PoolSubpageelemSize 獲取捻艳,偏移量要增加 (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize 的值。

3.2.2 回收內(nèi)存塊

    void free(long handle, ByteBuffer nioBuffer) {
        int memoryMapIdx = memoryMapIdx(handle);
        int bitmapIdx = bitmapIdx(handle);

        if (bitmapIdx != 0) { // free a subpage
            // bitmapIdx != 0 說明它是一個Tiny 或者 Small類型
            // 通過 memoryMapIdx 找到對應(yīng)的PoolSubpage
            PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
            assert subpage != null && subpage.doNotDestroy;

            // 獲取 PoolArena 中對應(yīng)尺寸容量的PoolSubpage鏈表頭
            PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
            synchronized (head) {

                // 釋放PoolSubpage中 bitmapIdx 對應(yīng)那一個內(nèi)存塊
                if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
                    // 如果 free(...) 方法返回true庆猫,說明這個PoolSubpage還在被使用认轨,
                    // 不能被回收,那么直接返回
                    return;
                }
            }
        }
        /**
         * 運行到這里月培,
         * 要么它是一個 Normal 類型內(nèi)存塊嘁字,那就釋放這個內(nèi)存塊。
         * 要么它是一個 Tiny 或者 Small類型节视,但是它對應(yīng) PoolSubpage 那一塊內(nèi)存塊都被釋放了拳锚,這里就釋放它
         */
        freeBytes += runLength(memoryMapIdx);
        // 將  memoryMapIdx 對應(yīng)節(jié)點設(shè)置回原來值,又可以進行內(nèi)存塊分配了
        setValue(memoryMapIdx, depth(memoryMapIdx));
        // 因為子節(jié)點內(nèi)存塊釋放寻行,更新父節(jié)點的可分配容量
        updateParentsFree(memoryMapIdx);

        if (nioBuffer != null && cachedNioBuffers != null &&
                cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
            cachedNioBuffers.offer(nioBuffer);
        }
    }
  • 如果是TinySmall 規(guī)格類型的內(nèi)存塊霍掺,那么就要使用 PoolSubpagefree(...) 方法釋放內(nèi)存塊。如果返回 true拌蜘,表示這個 PoolSubpage 還在使用杆烁,直接返回;如果返回 false简卧,表示這個 PoolSubpage 不在使用兔魂,可以被回收了,就要回收對應(yīng)的整個內(nèi)存塊举娩。
  • 增加當(dāng)前 PoolChunk 可用內(nèi)存字節(jié)數(shù) freeBytes析校。
  • 通過 setValue(...) 方法,將 memoryMapIdx 對應(yīng)節(jié)點值改成初始層數(shù)值铜涉,表示這個節(jié)點有可以分配了智玻。
  • 通過 updateParentsFree(memoryMapIdx) 方法,更新父節(jié)點的節(jié)點值芙代,因為子節(jié)點內(nèi)存塊被釋放吊奢,那么父節(jié)點可分配內(nèi)存大小變了。
    private void updateParentsFree(int id) {
        int logChild = depth(id) + 1;
        while (id > 1) {
            // 父節(jié)點
            int parentId = id >>> 1;
            // 左子節(jié)點
            byte val1 = value(id);
            // 右子節(jié)點
            byte val2 = value(id ^ 1);
            // 子節(jié)點的標準值
            logChild -= 1; // in first iteration equals log, subsequently reduce 1 from logChild as we traverse up

            if (val1 == logChild && val2 == logChild) {
                // 左子節(jié)點和右子節(jié)點都是標準值纹烹,說明兩個子節(jié)點都是空閑的
                // 那么父節(jié)點的容量就是兩倍
                setValue(parentId, (byte) (logChild - 1));
            } else {
                // 取左子節(jié)點和右子節(jié)點中較大的容量页滚,也是val比較小的值召边。
                byte val = val1 < val2 ? val1 : val2;
                setValue(parentId, val);
            }

            id = parentId;
        }
    }
  • 如果左子節(jié)點和右子節(jié)點都是標準值,說明兩個子節(jié)點都是空閑的裹驰,么父節(jié)點的容量就是兩倍隧熙。
  • 如果不是,那么就取左子節(jié)點和右子節(jié)點中較小值邦马,即可分配內(nèi)存大小更大贱鼻。

3.3 PoolArena

3.3.1 allocate 方法

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

先通過 newByteBuf(maxCapacity) 方法滋将,創(chuàng)建對應(yīng)類型的池化緩存區(qū)PooledByteBuf,然后調(diào)用allocate(cache, buf, reqCapacity) 方法進行內(nèi)存分配父丰。

  private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
        final int normCapacity = normalizeCapacity(reqCapacity);
        if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
            // 如果容量小于 pageSize 值,即是 Tiny 或者 Small類型
            int tableIdx;
            PoolSubpage<T>[] table;
            boolean tiny = isTiny(normCapacity);
            if (tiny) { // < 512
                if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                    // 先從當(dāng)前線程緩存中獲取掘宪,如果能得到,直接返回
                    return;
                }
                // 得到Tiny 類型索引魏滚,因為 Tiny 類型是每個相隔16,所以索引就是 normCapacity >>> 4
                tableIdx = tinyIdx(normCapacity);
                table = tinySubpagePools;
            } else {
                if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                    // 先從當(dāng)前線程緩存中獲取鼠次,如果能得到更哄,直接返回
                    return;
                }
                // 得到Small 類型索引腥寇,每個Small 類型是成倍擴展的,即 512 1024 2048 4096, 小于 pageSize 的大小
                tableIdx = smallIdx(normCapacity);
                table = smallSubpagePools;
            }

            // 得到符合容量尺寸的頭PoolSubpage
            final PoolSubpage<T> head = table[tableIdx];

            /**
             * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
             * {@link PoolChunk#free(long)} may modify the doubly linked list as well.
             */
            synchronized (head) {
                // 使用 synchronized 防止并發(fā)
                final PoolSubpage<T> s = head.next;
                if (s != head) {
                    // 有這種尺寸容量的 PoolSubpage麻敌, 容量必須是 normCapacity
                    assert s.doNotDestroy && s.elemSize == normCapacity;
                    // 從 PoolSubpage 中分配內(nèi)存
                    long handle = s.allocate();
                    assert handle >= 0;
                    // 將分配的
                    s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
                    incTinySmallAllocation(tiny);
                    return;
                }
            }
            synchronized (this) {
                // 運行到這里掂摔,表示目前沒有這個尺寸的 PoolSubpage,
                // 那么從PoolChunk 中分配
                allocateNormal(buf, reqCapacity, normCapacity);
            }

            // 增加計數(shù)
            incTinySmallAllocation(tiny);
            return;
        }
        if (normCapacity <= chunkSize) {
            if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
                ++allocationsNormal;
            }
        } else {
            // Huge類型的內(nèi)存塊肯定不會緩存在當(dāng)前線程中级历,直接調(diào)用 allocateHuge 分配
            allocateHuge(buf, reqCapacity);
        }
    }

這個方法看起來很復(fù)雜簇秒,但是其實邏輯很簡單:

  • 通過 normalizeCapacity(reqCapacity) 方法來將用戶申請內(nèi)存大小轉(zhuǎn)成規(guī)格化大小秀鞭,例如 18 就變成 32; 990 就變成 1024扛禽。
  • 根據(jù) Tiny,Small,NormalHuge 不同類型皱坛,進行不同內(nèi)存塊分配。
  • 對于 TinySmall 規(guī)格類型:
    • 先從線程緩存PoolThreadCache 中獲取掐场,如果獲取到贩猎,就直接返回,獲取不到就繼續(xù)下面步驟吭服。
    • 再通過tinySubpagePoolssmallSubpagePools 進行快速分配內(nèi)存塊,如果 tinySubpagePools 中有對應(yīng)的 PoolSubpage蝌戒,那么就直接分配沼琉,如果沒有,那么繼續(xù)下面步驟友鼻。
    • 通過 allocateNormal(buf, reqCapacity, normCapacity) 方法進行內(nèi)存塊的分配瑟慈。
  • 對于Normal 規(guī)格類型:
    • 先從線程緩存PoolThreadCache 中獲取,如果獲取到葛碧,就直接返回,獲取不到就繼續(xù)下面步驟蔗衡。
    • 通過 allocateNormal(buf, reqCapacity, normCapacity) 方法進行內(nèi)存塊的分配乳绕。
  • 對于Huge 規(guī)格類型:

    這種規(guī)格是沒有線程緩存的,所以直接通過 allocateHuge(buf, reqCapacity) 方法進行內(nèi)存塊的分配洋措。

3.3.2 normalizeCapacity 方法

    int normalizeCapacity(int reqCapacity) {
        if (reqCapacity < 0) {
            throw new IllegalArgumentException("capacity: " + reqCapacity + " (expected: 0+)");
        }

        // 大于 chunkSize,表示是一個 Huge 類型
        if (reqCapacity >= chunkSize) {
            // 是否需要進行內(nèi)存對齊
            return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity);
        }

        if (!isTiny(reqCapacity)) { // >= 512
            // Doubled

            // 得到與 reqCapacity 最近的 2的冪數(shù)王滤,
            // 如果 reqCapacity 就是2的冪數(shù),那么就是它自己
            int normalizedCapacity = reqCapacity;
            // 先減一第喳,防止 reqCapacity 就是 2的冪數(shù)踱稍,導(dǎo)致結(jié)果值是 reqCapacity 的兩步
            normalizedCapacity --;
            normalizedCapacity |= normalizedCapacity >>>  1;
            normalizedCapacity |= normalizedCapacity >>>  2;
            normalizedCapacity |= normalizedCapacity >>>  4;
            normalizedCapacity |= normalizedCapacity >>>  8;
            normalizedCapacity |= normalizedCapacity >>> 16;
            normalizedCapacity ++;

            if (normalizedCapacity < 0) {
                normalizedCapacity >>>= 1;
            }
            assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0;

            //
            return normalizedCapacity;
        }

        // 小于 512 數(shù),Tiny 類型的數(shù)扩淀,是否需要進行內(nèi)存對齊
        if (directMemoryCacheAlignment > 0) {
            return alignCapacity(reqCapacity);
        }

        // 能夠被 16 整除啤挎,那么就直接返回
        if ((reqCapacity & 15) == 0) {
            return reqCapacity;
        }

        // 結(jié)果值是 16 的倍數(shù)
        return (reqCapacity & ~15) + 16;
    }
  • 對于 Huge 規(guī)格類型,只考慮是否需要進行內(nèi)存對齊旺韭,即需要的內(nèi)存塊大小必須是某個數(shù)倍數(shù)掏觉;這個數(shù)必須是 2 的冪數(shù)。例如內(nèi)存對齊數(shù)directMemoryCacheAlignment16织盼,那么內(nèi)存塊大小必須能整除 16酱塔,也就是低四位都是 0
  • SmallNormal 規(guī)格類型羊娃,它們相隔都是1倍,那么只需要尋找最近的 2 的冪數(shù)就行了邮利。
  • Tiny 規(guī)格類型垃帅,最小值是16,因此只需要16的倍數(shù)就可以了方庭。

3.3.3 allocateNormal 方法

    private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
        // 從 PoolChunkList 中分配內(nèi)存
        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;
        }

        // 如果沒有從 PoolChunkList 中分配內(nèi)存,
        // 那么就要新創(chuàng)建 PoolChunk 對象械念,
        // 默認情況下 pageSize=8192 maxOrder=11 pageShifts=13 chunkSize=16777216
        PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
        boolean success = c.allocate(buf, reqCapacity, normCapacity);
        assert success;
        // 將新創(chuàng)建的 PoolChunk 添加到 qInit 中
        qInit.add(c);
    }
  • 先從 PoolChunkList 中尋找可用 PoolChunk 進行內(nèi)存分配订讼。
  • 找不到扇苞,那么就創(chuàng)建新的 PoolChunk 實例。
  • 通過 PoolChunkallocate(buf, reqCapacity, normCapacity) 方法進行內(nèi)存分配鳖敷;這個上面已經(jīng)介紹。
  • 最后將這個 PoolChunk 添加到 PoolChunkList 中棍潘。

3.3.4 allocateHuge 方法

    private void allocateHuge(PooledByteBuf<T> buf, int reqCapacity) {
        // 創(chuàng)建 Huge 類型的PoolChunk崖媚,不會放在內(nèi)存池中 unpooled = true
        PoolChunk<T> chunk = newUnpooledChunk(reqCapacity);
        activeBytesHuge.add(chunk.chunkSize());
        buf.initUnpooled(chunk, reqCapacity);
        allocationsHuge.increment();
    }

Huge 規(guī)格的內(nèi)存塊是不會進入內(nèi)存池的。

3.3.5 釋放內(nèi)存塊

    void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
        if (chunk.unpooled) {
            // 非池中內(nèi)存塊肴楷,直接回收
            int size = chunk.chunkSize();
            destroyChunk(chunk);
            activeBytesHuge.add(-size);
            deallocationsHuge.increment();
        } else {
            SizeClass sizeClass = sizeClass(normCapacity);
            // 根據(jù)不同內(nèi)存規(guī)格荠呐,將回收的內(nèi)存塊優(yōu)先放入線程緩存中
            if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
                // cached so not free it.
                return;
            }

            // 線程緩存已經(jīng)滿了,那么就釋放內(nèi)存塊
            freeChunk(chunk, handle, sizeClass, nioBuffer);
        }
    }

 void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer) {
        final boolean destroyChunk;
        synchronized (this) {
            switch (sizeClass) {
            case Normal:
                ++deallocationsNormal;
                break;
            case Small:
                ++deallocationsSmall;
                break;
            case Tiny:
                ++deallocationsTiny;
                break;
            default:
                throw new Error();
            }
            // 調(diào)用 PoolChunkList 方法進行內(nèi)存塊釋放呵恢,需要改變 PoolChunkList 中的一些值
            destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer);
        }
        if (destroyChunk) {
            // destroyChunk not need to be called while holding the synchronized lock.
            destroyChunk(chunk);
        }
    }
  • Huge 規(guī)格類型的內(nèi)存塊直接釋放媚创。
  • Tiny,SmallNormal 規(guī)格類型的內(nèi)存塊,優(yōu)先放入線程緩存中晌姚,如果對應(yīng)的線程緩存已經(jīng)滿了歇竟,那么才釋放。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宝磨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子唤锉,更是在濱河造成了極大的恐慌窿祥,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晒衩,死亡現(xiàn)場離奇詭異听系,居然都是意外死亡,警方通過查閱死者的電腦和手機靠胜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門浪漠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人址愿,你說我怎么就攤上這事“枭” “怎么了歌粥?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長土居。 經(jīng)常有香客問我嬉探,道長擦耀,這世上最難降的妖魔是什么涩堤? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任胎围,我火速辦了婚禮德召,結(jié)果婚禮上汽纤,老公的妹妹穿的比我還像新娘。我一直安慰自己蕴坪,他們只是感情好背传,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著续室,像睡著了一般谒养。 火紅的嫁衣襯著肌膚如雪买窟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天始绍,我揣著相機與錄音亏推,去河邊找鬼。 笑死吞杭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绢掰。 我是一名探鬼主播童擎,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼班挖!你這毒婦竟也來了芯砸?” 一聲冷哼從身側(cè)響起碴萧,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤末购,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后曹质,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體擎场,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年宅静,在試婚紗的時候發(fā)現(xiàn)自己被綠了站欺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡磷账,死狀恐怖贾虽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绰咽,我是刑警寧澤地粪,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站矛辕,受9級特大地震影響付魔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜几苍,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一妻坝、第九天 我趴在偏房一處隱蔽的房頂上張望惊窖。 院中可真熱鬧厘贼,春花似錦、人聲如沸嘴秸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽执解。三九已至纲酗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間耕姊,已是汗流浹背栅葡。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工欣簇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人熊咽。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓横殴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親梨与。 傳聞我的和親對象是個殘疾皇子文狱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

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