深入理解Node.js垃圾回收與內(nèi)存管理

使用JavaScript進(jìn)行前端開(kāi)發(fā)時(shí)幾乎完全不需要關(guān)心內(nèi)存管理問(wèn)題餐曹,對(duì)于前端編程來(lái)說(shuō)蜜另,V8限制的內(nèi)存幾乎不會(huì)出現(xiàn)用完的情況古今,但是由于后端程序往往進(jìn)行的操作更加復(fù)雜蠕搜,并且長(zhǎng)期運(yùn)行在服務(wù)器不重啟轮蜕,如果不關(guān)注內(nèi)存管理昨悼,導(dǎo)致內(nèi)存泄漏,就算1GB跃洛,也會(huì)很快用盡率触。
Node.js構(gòu)建于V8引擎之上,因此本文首先講解V8引擎的內(nèi)存管理機(jī)制汇竭,了解底層原理后葱蝗,再講解Node開(kāi)發(fā)中的內(nèi)存管理與優(yōu)化穴张。

一、V8的內(nèi)存管理機(jī)制

1.1 內(nèi)存管理模型

Node程序運(yùn)行中两曼,此進(jìn)程占用的所有內(nèi)存稱為常駐內(nèi)存(Resident Set)皂甘。

  • 常駐內(nèi)存由以下部分組成:
    1. 代碼區(qū)(Code Segment):存放即將執(zhí)行的代碼片段
    2. 棧(Stack):存放局部變量
    3. 堆(Heap):存放對(duì)象、閉包上下文
    4. 堆外內(nèi)存:不通過(guò)V8分配悼凑,也不受V8管理叮贩。Buffer對(duì)象的數(shù)據(jù)就存放于此。


      V8內(nèi)存模型

除堆外內(nèi)存佛析,其余部分均由V8管理益老。

  • 棧(Stack)的分配與回收非常直接,當(dāng)程序離開(kāi)某作用域后寸莫,其棧指針下移(回退)捺萌,整個(gè)作用域的局部變量都會(huì)出棧,內(nèi)存收回膘茎。
  • 最復(fù)雜的部分是堆(Heap)的管理桃纯,V8使用垃圾回收機(jī)制進(jìn)行堆的內(nèi)存管理,也是開(kāi)發(fā)中可能造成內(nèi)存泄漏的部分披坏,是程序員的關(guān)注點(diǎn)态坦,也是本文的探討點(diǎn)。

通過(guò)process.memoryUsage()可以查看此Node進(jìn)程的內(nèi)存使用狀況:

內(nèi)存使用狀況

rss是Resident Set Size的縮寫(xiě)棒拂,為常駐內(nèi)存的總大小伞梯,heapTotal是V8為堆分配的總大小,heapUsed是已使用的堆大小帚屉∶战耄可以看到,rss是大于heapTotal的攻旦,因?yàn)閞ss包括且不限于堆喻旷。

1.2 堆內(nèi)存限制

默認(rèn)情況下,V8為堆分配的內(nèi)存不超過(guò)1.4G:64位系統(tǒng)1.4G牢屋,32位則僅分配0.7G且预。也就是說(shuō),如果你想使用Node程序讀一個(gè)2G的文件到內(nèi)存烙无,在默認(rèn)的V8配置下锋谐,是無(wú)法實(shí)現(xiàn)的。不過(guò)我們可以通過(guò)Node的啟動(dòng)命令更改V8為堆設(shè)置的內(nèi)存上限:

//更改老年代堆內(nèi)存
--max-old-space-size=3000 // 單位為MB
// 更改新生代堆內(nèi)存
--max-new-space-size=1024 // 單位為KB

堆的內(nèi)存上限在啟動(dòng)時(shí)就已經(jīng)決定皱炉,無(wú)法動(dòng)態(tài)更改怀估,想要更改,唯一的方法是關(guān)閉進(jìn)程,使用新的配置重新啟動(dòng)多搀。

1.3 V8的垃圾回收機(jī)制

垃圾回收機(jī)制演變至今歧蕉,已經(jīng)出現(xiàn)了數(shù)種垃圾回收算法,各有千秋康铭,適用于不同場(chǎng)景惯退,沒(méi)有一種垃圾回收算法能夠效率最優(yōu)于所有場(chǎng)景。因此研發(fā)者們按照存活時(shí)間長(zhǎng)短从藤,將對(duì)象分類(lèi)催跪,為每一類(lèi)特定的對(duì)象,制定其最適合的垃圾回收算法夷野,以提高垃圾回收總效率懊蒸。

  • 1.3.1 V8的內(nèi)存分代

    • V8將堆中的對(duì)象分為兩類(lèi):
      1. 新生代:年輕的新對(duì)象,未經(jīng)歷垃圾回收或僅經(jīng)歷過(guò)一次
      2. 老年代:存活時(shí)間長(zhǎng)的老對(duì)象悯搔,經(jīng)歷過(guò)一次或更多次垃圾回收的對(duì)象


    默認(rèn)情況下骑丸,V8為老年代分配的空間,大概是新生代的40多倍妒貌。
    新對(duì)象都會(huì)被分配到新生代中通危,當(dāng)新生代空間不足以分配新對(duì)象時(shí),將觸發(fā)新生代的垃圾回收灌曙。

  • 1.3.2 新生代的垃圾回收
    新生代中的對(duì)象主要通過(guò)Scavenge算法進(jìn)行垃圾回收菊碟,這是一種采用復(fù)制的方式實(shí)現(xiàn)內(nèi)存回收的算法。
    Scavenge算法將新生代的總空間一分為二在刺,只使用其中一個(gè)逆害,另一個(gè)處于閑置,等待垃圾回收時(shí)使用增炭。使用中的那塊空間稱為From忍燥,閑置的空間稱為To

    From與To各占一半

    當(dāng)新生代觸發(fā)垃圾回收時(shí)隙姿,V8將From空間中所有應(yīng)該存活下來(lái)的對(duì)象依次復(fù)制到To空間。

    • 有兩種情況不會(huì)將對(duì)象復(fù)制到To空間厂捞,而是晉升至老年代:
      1. 對(duì)象此前已經(jīng)經(jīng)歷過(guò)一次新生代垃圾回收输玷,這次依舊應(yīng)該存活,則晉升至老年代靡馁。
      2. To空間已經(jīng)使用了25%欲鹏,則將此對(duì)象直接晉升至老年代。


    From空間所有應(yīng)該存活的對(duì)象都復(fù)制完成后臭墨,原本的From空間將被釋放赔嚎,成為閑置空間,原本To空間則成為使用中空間,兩個(gè)空間進(jìn)行角色翻轉(zhuǎn)尤误。
    為何To空間使用超過(guò)25%時(shí)侠畔,就需要直接將對(duì)象復(fù)制到老年代呢?因?yàn)門(mén)o空間完成垃圾回收后將會(huì)翻轉(zhuǎn)為From空間损晤,新的對(duì)象分配都在此處進(jìn)行软棺,如果沒(méi)有足夠的空閑空間,將會(huì)影響程序的新對(duì)象分配尤勋。
    因?yàn)镾cavenge只復(fù)制活著的對(duì)象喘落,而根據(jù)統(tǒng)計(jì)學(xué)指導(dǎo),新生代中大多數(shù)對(duì)象壽命都不長(zhǎng)最冰,長(zhǎng)期存活對(duì)象少瘦棋,則需要復(fù)制的對(duì)象相對(duì)來(lái)說(shuō)很少,因此總體來(lái)說(shuō)暖哨,新生代使用Scavenge算法的效率非常高兽狭。且由于Scavenge是依次連續(xù)復(fù)制,所以To空間永遠(yuǎn)不會(huì)存在內(nèi)存碎片鹿蜀。
    不過(guò)由于Scavenge會(huì)將空間對(duì)半劃分箕慧,所以此算法的空間利用率較低。

  • 1.3.3 老年代的垃圾回收
    在老年代中的對(duì)象茴恰,至少都已經(jīng)歷過(guò)一次甚至更多次垃圾回收颠焦,相對(duì)于新生代中的對(duì)象,它們有更大的概率繼續(xù)存活往枣,只有相對(duì)少數(shù)的對(duì)象面臨死亡伐庭,且由于老年代的堆內(nèi)存是新生代的幾十倍,其中生活著大量對(duì)象分冈,因此如果使用Scavenge算法回收老年代圾另,將會(huì)面臨大量的存活對(duì)象需要復(fù)制的情況,將老年代空間對(duì)半劃分雕沉,也會(huì)浪費(fèi)相當(dāng)大的空間集乔,效率低下。因此老年代垃圾回收主要采用標(biāo)記清除(Mark-Sweep)標(biāo)記整理(Mark-Compact)坡椒。
    這兩種方式并非互相替代關(guān)系扰路,而是配合關(guān)系,在不同情況下倔叼,選擇不同方式汗唱,交替配合以提高回收效率。
    新生代中死亡對(duì)象占多數(shù)丈攒,因此采用Scavenge算法只處理存活對(duì)象哩罪,提高效率授霸。老年代中存活對(duì)象占多數(shù),于是采用標(biāo)記清除算法只處理死亡對(duì)象际插,提高效率碘耳。
    當(dāng)老年代的垃圾回收被觸發(fā)時(shí),V8會(huì)將需要存活對(duì)象打上標(biāo)記腹鹉,然后將沒(méi)有標(biāo)記的對(duì)象藏畅,也就是需要死亡的對(duì)象,全部擦除功咒,一次標(biāo)記清除式回收就完成了:

    灰色為存活對(duì)象愉阎,白色為清除后的閑置空間

    一切看起來(lái)都完美了,可是隨著程序的繼續(xù)運(yùn)行力奋,卻會(huì)出現(xiàn)一個(gè)問(wèn)題:被清除的對(duì)象遍布各個(gè)內(nèi)存地址榜旦,空間有大有小,其閑置空間不連續(xù)景殷,產(chǎn)生了很多內(nèi)存碎片溅呢。當(dāng)需要將一個(gè)足夠大的對(duì)象晉升至老年代時(shí),無(wú)法找到一個(gè)足夠大的連續(xù)空間安置這個(gè)對(duì)象猿挚。
    為了解決這種空間碎片的問(wèn)題咐旧,就出現(xiàn)了標(biāo)記整理算法。它是在標(biāo)記清除的基礎(chǔ)上演變而來(lái)绩蜻,當(dāng)清理了死亡對(duì)象后铣墨,它會(huì)將所有存活對(duì)象往一端移動(dòng),使其內(nèi)存空間緊挨办绝,另一端就成為了連續(xù)內(nèi)存:

    雖然標(biāo)記整理算法可以避免空間碎片伊约,但是卻需要依次移動(dòng)對(duì)象,效率比標(biāo)記清除算法更低孕蝉,因此大多數(shù)情況下V8會(huì)使用標(biāo)記清理算法屡律,當(dāng)空間碎片不足以安放新晉升對(duì)象時(shí),才會(huì)觸發(fā)標(biāo)記整理算法降淮。

  • 1.3.4 增量標(biāo)記(Incremental Marking)
    早期V8在垃圾回收階段超埋,采用全停頓(stop the world),也就是垃圾回收時(shí)程序運(yùn)行會(huì)被暫停骤肛。這在JavaScript還僅被用于瀏覽器端開(kāi)發(fā)時(shí)纳本,并沒(méi)有什么明顯的缺點(diǎn),前端開(kāi)發(fā)使用的內(nèi)存少腋颠,大多數(shù)時(shí)候僅觸發(fā)新生代垃圾回收,速度快吓笙,卡頓幾乎感覺(jué)不到淑玫。但是對(duì)于Node程序,使用內(nèi)存更多,在老年代垃圾回收時(shí)絮蒿,全停頓很容易帶來(lái)明顯的程序遲滯尊搬,標(biāo)記階段很容易就會(huì)超過(guò)100ms,因此V8引入了增量標(biāo)記土涝,將標(biāo)記階段分為若干小步驟佛寿,每個(gè)步驟控制在5ms內(nèi),每運(yùn)行一段時(shí)間標(biāo)記動(dòng)作但壮,就讓JavaScript程序執(zhí)行一會(huì)兒冀泻,如此交替,明顯地提高了程序流暢性蜡饵,一定程度上避免了長(zhǎng)時(shí)間卡頓弹渔。

二、Node開(kāi)發(fā)中的內(nèi)存管理與優(yōu)化

2.1 手動(dòng)變量銷(xiāo)毀

當(dāng)任一作用域存活于作用域棧(作用域鏈)時(shí)溯祸,其中的變量都不會(huì)被銷(xiāo)毀肢专,其引用的數(shù)據(jù)也會(huì)一直被變量關(guān)聯(lián),得不到GC焦辅。有的作用域存活時(shí)間非常長(zhǎng)(越是棧底博杖,存活時(shí)間越長(zhǎng),最長(zhǎng)的是全局作用域)筷登,但是其中的某些變量也許在某一時(shí)刻后就沒(méi)有用處了剃根,因此建議手動(dòng)設(shè)置為null盗飒,斷開(kāi)引用鏈接赤拒,使得V8可以及時(shí)GC釋放內(nèi)存。
注意蛙婴,不使用var聲明的變量镣丑,都會(huì)成為全局對(duì)象的屬性舔糖。前端開(kāi)發(fā)中全局對(duì)象為window,Node中全局對(duì)象為global莺匠,如果global中有屬性已經(jīng)沒(méi)有用處了金吗,一定要設(shè)置為null,因?yàn)槿肿饔糜蛑挥械鹊匠绦蛲V惯\(yùn)行趣竣,才會(huì)銷(xiāo)毀摇庙。
Node中,當(dāng)一個(gè)模塊被引入遥缕,這個(gè)模塊就會(huì)被緩存在內(nèi)存中卫袒,提高下次被引用的速度。也就是說(shuō)单匣,一般情況下夕凝,整個(gè)Node程序中對(duì)同一個(gè)模塊的引用宝穗,都是同一個(gè)實(shí)例(instance),這個(gè)實(shí)例一直存活在內(nèi)存中码秉。所以逮矛,如果任意模塊中有變量已經(jīng)不再需要,最好手動(dòng)設(shè)置為null转砖,不然會(huì)白白占用內(nèi)存须鼎,成為“活著的死對(duì)象”。

2.2 慎用閉包

  • 2.2.1 V8的閉包實(shí)現(xiàn)
    先來(lái)看一段例子:
function outer(){
    var x = 1; // 真正的局部變量:outer執(zhí)行完后立即死亡
    var y = 2; // 上下文變量:閉包死亡后才會(huì)死亡
    // 返回一個(gè)閉包
    return function(){
      console.log(y); // 使用了外層函數(shù)的變量 y
    }
}
var inner = outer(); // 通過(guò)inner變量持有閉包

有不少開(kāi)發(fā)者認(rèn)為府蔗,如果閉包被引用晋控,那么閉包的外部函數(shù)也不會(huì)被釋放,其中的所有變量都不會(huì)被銷(xiāo)毀礁竞,比如我通過(guò)inner變量持有了閉包糖荒,此時(shí)outer中的 x、y 均活在內(nèi)存中模捂,不會(huì)被銷(xiāo)毀捶朵。事實(shí)真是這樣嗎?
答案是:在V8的實(shí)現(xiàn)中狂男,當(dāng)outer執(zhí)行完畢综看,x 立即死亡,僅有 y 存活岖食。
V8是這么做的:
當(dāng)程序進(jìn)入一個(gè)函數(shù)時(shí)红碑,將會(huì)為這個(gè)函數(shù)創(chuàng)建一個(gè)上下文(Context),初始狀態(tài)這個(gè)Context是空的泡垃,當(dāng)讀到這個(gè)函數(shù)(outer)中的閉包聲明時(shí)析珊,將會(huì)把此閉包(inner)中使用的外部變量,加入Context蔑穴。在上面的例子中忠寻,由于inner函數(shù)使用了變量 y ,因此會(huì)將 y 加入Context存和。outer內(nèi)部所有的閉包奕剃,都會(huì)持有這個(gè)Context


每一個(gè)閉包都會(huì)引用其外部函數(shù)的Context捐腿,以此訪問(wèn)需要讀取的外部變量纵朋。被閉包捕捉,加入Context中的變量茄袖,我們稱為Context變量操软,分配在堆。而真正的 局部變量(local variable)是 x 宪祥,保存在棧寺鸥,當(dāng)outer執(zhí)行完畢后猪钮,其信息出棧品山,變量 x 自然銷(xiāo)毀胆建,而Context被閉包引用,如果有任何一個(gè)閉包存活肘交,Context都將存活笆载,y 將不會(huì)被銷(xiāo)毀。
舉一反三涯呻,再來(lái)看一個(gè)更復(fù)雜的例子:

function outer () { 
    var x; // 真正的局部變量
    var y; // context variable, 被inner1使用
    var z; // context variable, 被inner2使用
    function inner1 () { 
      use(y); 
    } 
    function inner2 () { 
      use(z); 
    } 
    function inner3 () { 
      /* 雖然函數(shù)體為空凉驻,但是作為閉包,依舊引用outer的Context */
    } 
    return [inner1, inner2, inner3];
}

x复罐、y涝登、z 三個(gè)變量何時(shí)死亡?
x 在outer執(zhí)行完后立即死亡效诅, y胀滚、z 需要等到inner1、inner2乱投、inner3三個(gè)閉包都死亡后咽笼,才會(huì)死亡。
x 未被任何閉包使用戚炫,因此是一個(gè)真正的局部變量剑刑,保存在棧,函數(shù)執(zhí)行完即被出棧死亡双肤。由于 y施掏、z 兩個(gè)變量分別被inner1、inner2使用茅糜,則它們會(huì)被加入outer的Context七芭。所有閉包都會(huì)引用外部函數(shù)的Context,即使inner3為空限匣,不使用任何外部函數(shù)的變量抖苦,也會(huì)引用Context,所以需要等到三個(gè)閉包都死亡后米死,y锌历、z 才會(huì)死亡。


因此:如果較大的對(duì)象成為了Context變量峦筒,建議嚴(yán)格控制引用此Context的閉包生命周期以及閉包數(shù)量究西,或在不需要時(shí),設(shè)置為null物喷,以免引起較多內(nèi)存的長(zhǎng)期占用卤材。

  • 2.2.2 避免深層閉包嵌套
function outer() { 
    var x = HUGE; // 超大對(duì)象
    function inner() { 
      var y = GIANT; // 大對(duì)象
      use(x); // x 需要使用遮斥,需要成為Context變量
      function innerF() { 
        use(y); // y 需要使用,需要成為Context變量
      } 
      function innerG() { 
        /* 空函數(shù)體 */
      } 
      return innerG; 
    } 
    return inner();
}
var o = outer(); // HUGE and GIANT 均得不到釋放

變量 o 持有的是innerG閉包扇丛,innerG持有著inner的Context术吗,且內(nèi)部閉包的Context會(huì)持有外部閉包的Context,產(chǎn)生Context鏈帆精。

上下文鏈

為了減輕GC壓力较屿,建議避免過(guò)深嵌套函數(shù)/閉包,或及早手動(dòng)斷開(kāi)Context變量所引用的大對(duì)象卓练。

2.3 大內(nèi)存使用

  • 2.3.1 使用stream
    當(dāng)我們需要操作大文件隘蝎,應(yīng)該利用Node提供的stream以及其管道方法,防止一次性讀入過(guò)多數(shù)據(jù)襟企,占用堆空間嘱么,增大堆內(nèi)存壓力。

  • 2.3.2 使用Buffer
    Buffer是操作二進(jìn)制數(shù)據(jù)的對(duì)象顽悼,不論是字符串還是圖片曼振,底層都是二進(jìn)制數(shù)據(jù),因此Buffer可以適用于任何類(lèi)型的文件操作表蝙。
    Buffer對(duì)象本身屬于普通對(duì)象拴测,保存在堆,由V8管理府蛇,但是其儲(chǔ)存的數(shù)據(jù)集索,則是保存在堆外內(nèi)存,是有C++申請(qǐng)分配的汇跨,因此不受V8管理务荆,也不需要被V8垃圾回收,一定程度上節(jié)省了V8資源穷遂,也不必在意堆內(nèi)存限制函匕。

參考資料:

**
本人技術(shù)有限,且技術(shù)更新很快蚪黑,如果文中存在錯(cuò)誤或者不足盅惜,歡迎大家指正,相互交流忌穿。
郵箱:hjaurum@gmail.com
**

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抒寂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子掠剑,更是在濱河造成了極大的恐慌屈芜,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異井佑,居然都是意外死亡属铁,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)躬翁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)焦蘑,“玉大人,你說(shuō)我怎么就攤上這事姆另±撸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵迹辐,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我甚侣,道長(zhǎng)明吩,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任殷费,我火速辦了婚禮印荔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘详羡。我一直安慰自己仍律,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布实柠。 她就那樣靜靜地躺著水泉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窒盐。 梳的紋絲不亂的頭發(fā)上草则,一...
    開(kāi)封第一講書(shū)人閱讀 52,328評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音蟹漓,去河邊找鬼炕横。 笑死,一個(gè)胖子當(dāng)著我的面吹牛葡粒,可吹牛的內(nèi)容都是我干的份殿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嗽交,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼卿嘲!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起轮纫,我...
    開(kāi)封第一講書(shū)人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤腔寡,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后掌唾,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體放前,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忿磅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了凭语。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葱她。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖似扔,靈堂內(nèi)的尸體忽然破棺而出吨些,到底是詐尸還是另有隱情,我是刑警寧澤炒辉,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布豪墅,位于F島的核電站,受9級(jí)特大地震影響黔寇,放射性物質(zhì)發(fā)生泄漏偶器。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一缝裤、第九天 我趴在偏房一處隱蔽的房頂上張望屏轰。 院中可真熱鬧,春花似錦憋飞、人聲如沸霎苗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)唁盏。三九已至,卻和暖如春瘤睹,著一層夾襖步出監(jiān)牢的瞬間升敲,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工轰传, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驴党,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓获茬,卻偏偏與公主長(zhǎng)得像港庄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子恕曲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

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