自頂向下深入分析Netty(十)--PoolArena

上一節(jié)講述了jemalloc的思想,本節(jié)將分析Netty的實(shí)現(xiàn)細(xì)節(jié)吃媒。在Netty實(shí)現(xiàn)中,相關(guān)的類(lèi)都加上了前綴Pool吕喘,比如PoolArenaPoolChunk等,本節(jié)分析PoolArena的源碼實(shí)現(xiàn)細(xì)節(jié)树枫。

首先看類(lèi)簽名:

    abstract class PoolArena<T> implements PoolArenaMetric

該類(lèi)是一個(gè)抽象類(lèi)档址,這是因?yàn)?code>ByteBuf分為Heap和Direct,所以PoolArena同樣分為兩類(lèi):Heap和Direct闻察。該類(lèi)實(shí)現(xiàn)的接口PoolArenaMetric是一些信息的測(cè)度統(tǒng)計(jì)拱礁,忽略這些信息不再分析。
其中的關(guān)鍵成員變量如下:

    private final int maxOrder; // chunk相關(guān)滿二叉樹(shù)的高度
    final int pageSize; // 單個(gè)page的大小
    final int pageShifts; // 用于輔助計(jì)算
    final int chunkSize; // chunk的大小
    final int subpageOverflowMask; // 用于判斷請(qǐng)求是否為Small/Tiny
    final int numSmallSubpagePools; // small請(qǐng)求的雙向鏈表頭個(gè)數(shù)
    final int directMemoryCacheAlignment; // 對(duì)齊基準(zhǔn)
    final int directMemoryCacheAlignmentMask; // 用于對(duì)齊內(nèi)存
    private final PoolSubpage<T>[] tinySubpagePools; // Subpage雙向鏈表
    private final PoolSubpage<T>[] smallSubpagePools; // Subpage雙向鏈表
    
    final PooledByteBufAllocator parent;

對(duì)于前述分析的如QINIT辕漂、Q0等chunk狀態(tài)呢灶,Netty使用PoolChunkList作為容器存放相同狀態(tài)的Chunk塊,相關(guān)變量如下:

    private final PoolChunkList<T> q050;
    private final PoolChunkList<T> q025;
    private final PoolChunkList<T> q000;
    private final PoolChunkList<T> qInit;
    private final PoolChunkList<T> q075;
    private final PoolChunkList<T> q100;

構(gòu)造方法如下:

    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 = new PoolSubpage[numTinySubpagePools];
        for (int i = 0; i < tinySubpagePools.length; i ++) {
            tinySubpagePools[i] = newSubpagePoolHead(pageSize);
        }

        numSmallSubpagePools = pageShifts - 9;
        smallSubpagePools = new PoolSubpage[numSmallSubpagePools];
        for (int i = 0; i < smallSubpagePools.length; i ++) {
            smallSubpagePools[i] = newSubpagePoolHead(pageSize);
        }
        
        initPoolChunkList();
    }
    
    private PoolSubpage<T> newSubpagePoolHead(int pageSize) {
        PoolSubpage<T> head = new PoolSubpage<T>(pageSize);
        head.prev = head;
        head.next = head;
        return head;
    }

其中initPoolChunkList()如下:

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

這段代碼實(shí)現(xiàn)如下圖所示的雙向鏈表:


狀態(tài)轉(zhuǎn)移

Netty使用一個(gè)枚舉來(lái)表示每次請(qǐng)求大小的類(lèi)別:

    enum SizeClass {
        Tiny,
        Small,
        Normal
        // 除此之外的請(qǐng)求為Huge
    }

根據(jù)請(qǐng)求分配大小判斷所屬分類(lèi)的代碼如下钉嘹,體會(huì)其中的位運(yùn)算:

    // capacity < pageSize
    boolean isTinyOrSmall(int normCapacity) {
        // subpageOverflowMask = ~(pageSize - 1)
        return (normCapacity & subpageOverflowMask) == 0;
    }

    // normCapacity < 512
    static boolean isTiny(int normCapacity) {
        return (normCapacity & 0xFFFFFE00) == 0;
    }
    
    // capacity <= chunkSize
    boolean isNormal(int normCapacity){
        return normCapacity <= chunkSize;
    }

對(duì)容量進(jìn)行規(guī)范化的代碼如下:

    int normalizeCapacity(int reqCapacity) {
        // Huge 直接返回(直接內(nèi)存需要對(duì)齊)
        if (reqCapacity >= chunkSize) {
            return directMemoryCacheAlignment == 0 ? reqCapacity : 
                              alignCapacity(reqCapacity);
        }

        // Small和Normal 規(guī)范化到大于2的n次方的最小值
        if (!isTiny(reqCapacity)) { // >= 512
            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;
        }
        
        // Tiny且直接內(nèi)存需要對(duì)齊
        if (directMemoryCacheAlignment > 0) {
            return alignCapacity(reqCapacity);
        }

        // Tiny且已經(jīng)是16B的倍數(shù)
        if ((reqCapacity & 15) == 0) {
            return reqCapacity;
        }
        
        // Tiny不是16B的倍數(shù)則規(guī)范化到16B的倍數(shù)
        return (reqCapacity & ~15) + 16;
    }

規(guī)范化的結(jié)果可查看請(qǐng)求分類(lèi)圖填抬,實(shí)現(xiàn)中使用了大量位運(yùn)算,請(qǐng)仔細(xì)體會(huì)隧期。另外飒责,直接內(nèi)存對(duì)齊后的請(qǐng)求容量為基準(zhǔn)的倍數(shù),比如基準(zhǔn)為64B仆潮,則分配的內(nèi)存都需要為64B的整數(shù)倍宏蛉,也就是常說(shuō)的按64字節(jié)對(duì)齊,實(shí)現(xiàn)代碼如下(依然使用位運(yùn)算):

    int alignCapacity(int reqCapacity) {
        // directMemoryCacheAlignmentMask = cacheAlignment - 1;
        int delta = reqCapacity & directMemoryCacheAlignmentMask;
        return delta == 0 ? reqCapacity : reqCapacity + directMemoryCacheAlignment - delta;
    }

對(duì)于Small和Tiny的請(qǐng)求性置,隨著請(qǐng)求的分配拾并,PoolArena可能會(huì)形成如下的雙向循環(huán)鏈表:

Small請(qǐng)求雙向鏈表

其中的每個(gè)節(jié)點(diǎn)都是PoolSubpage,在jemalloc的介紹中,說(shuō)明Subpage會(huì)以第一次請(qǐng)求分配的大小為基準(zhǔn)劃分嗅义,之后也只能進(jìn)行這個(gè)基準(zhǔn)大小的內(nèi)存分配屏歹。在PoolArena中繼續(xù)對(duì)PoolSubpage進(jìn)行分組,將相同基準(zhǔn)的PoolSubpage連接成為雙向循環(huán)鏈表之碗,便于管理和內(nèi)存分配蝙眶。需要注意的是鏈表頭結(jié)點(diǎn)head是一個(gè)特殊的PoolSubpage,不進(jìn)行實(shí)際的內(nèi)存分配任務(wù)褪那。得到鏈表head節(jié)點(diǎn)的代碼如下:

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

        return table[tableIdx];
    }

明白了這些幽纷,繼續(xù)分析重要的內(nèi)存分配方法allocate():

    private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, 
                                              final int reqCapacity) {
        // 規(guī)范化請(qǐng)求容量
        final int normCapacity = normalizeCapacity(reqCapacity);
        // capacity < pageSize, Tiny/Small請(qǐng)求
        if (isTinyOrSmall(normCapacity)) { 
            int tableIdx;
            PoolSubpage<T>[] table;
            boolean tiny = isTiny(normCapacity);
            if (tiny) { // < 512 Tiny請(qǐng)求
                if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                    return; // 嘗試從ThreadCache進(jìn)行分配
                }
                tableIdx = tinyIdx(normCapacity);
                table = tinySubpagePools;
            } else { // Small請(qǐng)求
                if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                    return; // 嘗試從ThreadCache進(jìn)行分配
                }
                tableIdx = smallIdx(normCapacity);
                table = smallSubpagePools;
            }

            // 分組的Subpage雙向鏈表的頭結(jié)點(diǎn)
            final PoolSubpage<T> head = table[tableIdx];

            synchronized (head) {   // 鎖定防止其他操作修改head結(jié)點(diǎn)
                final PoolSubpage<T> s = head.next;
                if (s != head) {
                    assert s.doNotDestroy && s.elemSize == normCapacity;
                    long handle = s.allocate(); // 進(jìn)行分配
                    assert handle >= 0;
                    s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
                    return;
                }
            }
            
            synchronized (this) {
                // 雙向循環(huán)鏈表還沒(méi)初始化,使用normal分配
                allocateNormal(buf, reqCapacity, normCapacity);
            }
            return;
        }
        
        // Normal請(qǐng)求
        if (normCapacity <= chunkSize) {
            if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                return; // 嘗試從ThreadCache進(jìn)行分配
            }
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
            }
        } else {
            // Huge請(qǐng)求直接分配
            allocateHuge(buf, reqCapacity);
        }
    }

對(duì)于Normal和Huge的分配細(xì)節(jié)如下:

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

        // 無(wú)Chunk或已存Chunk不能滿足分配博敬,新增一個(gè)Chunk
        PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
        long handle = c.allocate(normCapacity);
        assert handle > 0;
        c.initBuf(buf, handle, reqCapacity);
        qInit.add(c);   // Chunk初始狀態(tài)為QINIT
    }
    
    private void allocateHuge(PooledByteBuf<T> buf, int reqCapacity) {
        PoolChunk<T> chunk = newUnpooledChunk(reqCapacity);
        buf.initUnpooled(chunk, reqCapacity);
    }

總結(jié)一下內(nèi)存分配過(guò)程:

  1. 對(duì)于Tiny/Small友浸、Normal大小的請(qǐng)求,優(yōu)先從線程緩存中分配偏窝。
  2. 沒(méi)有從緩存中得到分配的Tiny/Small請(qǐng)求收恢,會(huì)從以第一次請(qǐng)求大小為基準(zhǔn)進(jìn)行分組的Subpage雙向鏈表中進(jìn)行分配;如果雙向鏈表還沒(méi)初始化祭往,則會(huì)使用Normal請(qǐng)求分配Chunk塊中的一個(gè)Page派诬,Page以請(qǐng)求大小為基準(zhǔn)進(jìn)行切分并分配第一塊內(nèi)存,然后加入到雙向鏈表中链沼。
  3. 沒(méi)有從緩存中得到分配的Normal請(qǐng)求默赂,則會(huì)使用伙伴算法分配滿足要求的連續(xù)Page塊。
  4. 對(duì)于Huge請(qǐng)求括勺,則直接使用Unpooled直接分配缆八。

內(nèi)存分配過(guò)程分析完畢,接著分析內(nèi)存釋放:

    void free(PoolChunk<T> chunk, long handle, int normCapacity, PoolThreadCache cache) {
        if (chunk.unpooled) {   // Huge
            int size = chunk.chunkSize();
            destroyChunk(chunk);    // 模板方法疾捍,子類(lèi)實(shí)現(xiàn)具體釋放過(guò)程
        } else {    // Normal, Small/Tiny
            SizeClass sizeClass = sizeClass(normCapacity);
            if (cache != null && cache.add(this, chunk, handle, normCapacity, sizeClass)) {
                return;  // 可以緩存則不釋放
            }
            // 否則釋放
            freeChunk(chunk, handle, sizeClass);
        }
    }
    
    void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass) {
        final boolean destroyChunk;
        synchronized (this) {
            // parent為所屬的chunkList奈辰,destroyChunk為true表示Chunk內(nèi)存使用裝填
            // 從QINIT->Q0->...->Q0,最后釋放
            destroyChunk = !chunk.parent.free(chunk, handle);
        }
        if (destroyChunk) {
            destroyChunk(chunk); // 模板方法乱豆,子類(lèi)實(shí)現(xiàn)具體釋放過(guò)程
        }
    }

需要注意的是finalize()奖恰,該方法是Object中的方法,在對(duì)象被GC回收時(shí)調(diào)用宛裕,可知在該方法中需要清理資源瑟啃,本類(lèi)中主要清理內(nèi)存,代碼如下:

    protected final void finalize() throws Throwable {
        try {
            super.finalize();
        } finally {
            destroyPoolSubPages(smallSubpagePools); 
            destroyPoolSubPages(tinySubpagePools);
            destroyPoolChunkLists(qInit, q000, q025, q050, q075, q100);
        }
    }

    private static void destroyPoolSubPages(PoolSubpage<?>[] pages) {
        for (PoolSubpage<?> page : pages) {
            page.destroy();
        }
    }

    private void destroyPoolChunkLists(PoolChunkList<T>... chunkLists) {
        for (PoolChunkList<T> chunkList: chunkLists) {
            chunkList.destroy(this);
        }
    }

此外揩尸,當(dāng)PooledByteBuf容量擴(kuò)增時(shí)蛹屿,內(nèi)存需要重新分配,代碼如下:

    void reallocate(PooledByteBuf<T> buf, int newCapacity, boolean freeOldMemory) {
        int oldCapacity = buf.length;
        if (oldCapacity == newCapacity) {
            return;
        }

        PoolChunk<T> oldChunk = buf.chunk;
        long oldHandle = buf.handle;
        T oldMemory = buf.memory;
        int oldOffset = buf.offset;
        int oldMaxLength = buf.maxLength;
        int readerIndex = buf.readerIndex();
        int writerIndex = buf.writerIndex();
        
        // 分配新內(nèi)存
        allocate(parent.threadCache(), buf, newCapacity);
        // 將老數(shù)據(jù)copy到新內(nèi)存
        if (newCapacity > oldCapacity) {
            memoryCopy(oldMemory, oldOffset,
                        buf.memory, buf.offset, oldCapacity);
        } else if (newCapacity < oldCapacity) {
            if (readerIndex < newCapacity) {
                if (writerIndex > newCapacity) {
                    writerIndex = newCapacity;
                }
                memoryCopy(oldMemory, oldOffset + readerIndex,
                            buf.memory, buf.offset + readerIndex, writerIndex - readerIndex);
            } else {
                readerIndex = writerIndex = newCapacity;
            }
        }
        
        // 重新設(shè)置讀寫(xiě)索引
        buf.setIndex(readerIndex, writerIndex);

        // 如有必要岩榆,釋放老的內(nèi)存
        if (freeOldMemory) {
            free(oldChunk, oldHandle, oldMaxLength, buf.cache);
        }
    }

最后错负,由于該類(lèi)是一個(gè)抽象類(lèi)坟瓢,其中的抽象方法如下:

    // 新建一個(gè)Chunk,Tiny/Small犹撒,Normal請(qǐng)求請(qǐng)求分配時(shí)調(diào)用
    protected abstract PoolChunk<T> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize);
    // 新建一個(gè)Chunk折联,Huge請(qǐng)求分配時(shí)調(diào)用
    protected abstract PoolChunk<T> newUnpooledChunk(int capacity);
    // 
    protected abstract PooledByteBuf<T> newByteBuf(int maxCapacity);
    // 復(fù)制內(nèi)存,當(dāng)ByteBuf擴(kuò)充容量時(shí)調(diào)用
    protected abstract void memoryCopy(T src, int srcOffset, T dst, int dstOffset, int length);
    // 銷(xiāo)毀Chunk识颊,釋放內(nèi)存時(shí)調(diào)用
    protected abstract void destroyChunk(PoolChunk<T> chunk);
    // 判斷子類(lèi)實(shí)現(xiàn)Heap還是Direct
    protectted abstract boolean isDirect();

該類(lèi)的兩個(gè)子類(lèi)分別是HeapArenaDirectArena诚镰,根據(jù)底層不同而實(shí)現(xiàn)不同的抽象方法。方法簡(jiǎn)單易懂谊囚,不再列出代碼怕享。

相關(guān)鏈接:

  1. JEMalloc分配算法
  2. PoolChunk
  3. PoolChunkList
  4. PoolSubpage
  5. PooThreadCache
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末执赡,一起剝皮案震驚了整個(gè)濱河市镰踏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沙合,老刑警劉巖奠伪,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異首懈,居然都是意外死亡绊率,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)究履,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)滤否,“玉大人,你說(shuō)我怎么就攤上這事最仑∶臧常” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵泥彤,是天一觀的道長(zhǎng)欲芹。 經(jīng)常有香客問(wèn)我,道長(zhǎng)吟吝,這世上最難降的妖魔是什么菱父? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮剑逃,結(jié)果婚禮上浙宜,老公的妹妹穿的比我還像新娘。我一直安慰自己蛹磺,他們只是感情好梆奈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著称开,像睡著了一般亩钟。 火紅的嫁衣襯著肌膚如雪乓梨。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天清酥,我揣著相機(jī)與錄音扶镀,去河邊找鬼。 笑死焰轻,一個(gè)胖子當(dāng)著我的面吹牛臭觉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播辱志,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蝠筑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了揩懒?” 一聲冷哼從身側(cè)響起什乙,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎已球,沒(méi)想到半個(gè)月后臣镣,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡智亮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年忆某,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阔蛉。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弃舒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出状原,到底是詐尸還是另有隱情聋呢,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布遭笋,位于F島的核電站坝冕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瓦呼。R本人自食惡果不足惜喂窟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望央串。 院中可真熱鬧磨澡,春花似錦、人聲如沸质和。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)饲宿。三九已至厦酬,卻和暖如春胆描,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仗阅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工昌讲, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人减噪。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓短绸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親筹裕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子醋闭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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