你的 JVM 還好嗎?GC 初步診斷

前言

JVM的GC機制絕對是很多程序員的福音,它讓Java程序員省去了自己回收垃圾的煩惱。從而可以把大部分時間專注業(yè)務身上,大大提高了業(yè)務開發(fā)速度,讓產品需求盡快的落地搶占市場。但是也正因為如此,導致很多Java程序員對JVM和GC知之甚少,以我愚見大家對JVM&GC不夠了解的有幾個原因:

門檻太高。我們平常接觸的spring,dubbo,java集合&J.U.C,網上都有無數(shù)優(yōu)秀的文章對其深入的分析。而且都是基于Java語言,我們在學習的過程中,可以自己很容易的debug源碼更深入的了解。但是JVM則不然,它是C++開發(fā)的,能同時掌握C++和Java的程序員還是很少的,自己也不太好debug分析它的源碼(就是編譯jvm源碼,都要折騰一番)。

有價值的系列文章太少。網上幾乎沒有完整體系的文章,優(yōu)秀的書籍也很少(可能大家聽過或者看過最多的就是周志明的?深入理解Java虛擬機?,這確實是JVM領域比較少見的佳作)。

接觸的太少。雖然我們天天寫Java代碼,你寫的每行代碼JVM都會參與工作。但是很少進行GC調優(yōu),因為JVM是如此的優(yōu)秀,絕大部分情況下,它只是默默的做你背后的女人(即使出問題了,也輪不到我們去排查,扎心了)。

JVM&GC,很多人沒有去了解它,很多人也沒機會去了解它,甚至有部分人都不原因去了解它。然而要想成為一名優(yōu)秀的Java程序員,了解JVM和它的GC機制,寫出JVM GC機制更喜歡的代碼。并且你能知道JVM這個背后的女人是否發(fā)脾氣了,還知道她發(fā)脾氣的原因,這是必須要掌握的一門技術,?是通往高級甚至優(yōu)秀必須具備的技能?。

這篇文章我不打算普及JVM&GC基礎,而是主要講解?如何初步診斷GC是否正常?,重點講解 診斷GC 。所以看這篇文章的前提,需要你對JVM有一定的了解,比如常用的垃圾回收器,Java堆的模型等。如果你還對JVM一無所知,并且確實想初步了解這門技術,那么請先花點時間看一下周志明的<<深入理解Java虛擬機>>,重點關注"?第二部分 自動內存管理機制?"。

GC概念糾正

初步診斷GC之前,先對GC中最常誤解的幾個概念普及一下。對GC機制有一定了解的同學都知道,GC主要有YoungGC,OldGC,F(xiàn)ullGC(還有G1中獨有的Mixed GC,收集整個young區(qū)以及部分Old區(qū),提及的概率相對少很多,本篇文章不打算講解),大概解釋一下這三種GC,因為很多很多的同學對OldGC和FullGC有非常大的誤解;

Young GC:應該是最沒有歧義的一種GC了,只是有些地方稱之為 Minor GC ,或者簡稱 YGC ,都是沒有問題的;

Old GC:截止Java發(fā)展到現(xiàn)在JDK9為止,?只單獨回收Old區(qū)的只有CMS GC?,并且我們常說的CMS是指它的 background collection 模式,這個模式包含CMS GC完整的5個階段:初始化標記,并發(fā)標記,重新標記,并發(fā)清理,并發(fā)重置。由CMS的5個階段可知,仍然有兩個階段需要STW,所以CMS并不是完全并發(fā),而是?Mostly Concurrent Mark Sweep?,G1出來之前,CMS絕對是OLTP系統(tǒng)標配的垃圾回收器。

FullGC:有些地方稱之為 Major GC ,Major GC通常是跟FullGC是等價的,都是收集整個GC堆。但因為HotSpot VM發(fā)展了這么多年,外界對各種名詞的解讀已經完全混亂了,當有人說“Major GC”的時候一定要問清楚他想要指的是上面的FullGC還是OldGC(參考R大的Major GC和Full GC的區(qū)別:https://www.zhihu.com/question/41922036/answer/93079526)。大家普遍對這個GC的誤解絕對是最大的(我可以說至少有80%的人都有誤解),首先對于ParallelOldGC即默認GC在Old滿了以后觸發(fā)的FullGC是沒有問題的,jstat命令查看輸出結果 FGC 的值也會相應的+1,即發(fā)生了一次FGC。?FGC誤解主要來自最常用的ParNew+CMS組合?,很多人誤解FullGC可能是受到 jstat 命令結果的影響,因為發(fā)生CMS GC時,F(xiàn)GC也會增大(但是會+2,這是因為CMS GC的初始化標記和重新標記都會完全的STW,從而FGC的值會+2)。但是,事實上這并沒有發(fā)生FullGC。?jstat命令結果中的FGC并不表示就一定發(fā)生了FullGC?,很有可能只是發(fā)生了CMS GC而已。事實上,F(xiàn)ullGC的觸發(fā)條件非常苛刻,?判斷是否發(fā)生了FullGC最好的方法是通過GC日志?,日志中如果有"full gc"的字樣,就表示一定發(fā)生了Full GC。所以?強烈建議生產環(huán)境開啟GC日志?,它的價值遠大于它對性能的影響(不用權衡這個影響有多大,開啟就對了)。

關于CMS的foreground collect模式,以及FullGC:

foreground collect

它發(fā)生的場景,比如業(yè)務線程請求分配內存,但是內存不夠了,于是可能觸發(fā)一次CMS GC,這個過程就必須要等待內存分配成功后線程才能繼續(xù)往下面走,因此整個過程必須STW,因此這種CMS GC整個過程都是暫停應用的,但是為了提高效率,它并不是每個階段都會走的,只走其中一些階段,這些省下來的階段主要是并行階段:Precleaning、AbortablePreclean,Resizing這幾個階段都不會經歷,但不管怎么說如果走了類似foreground這種CMS GC,那么整個過程業(yè)務線程都是不可用的,效率會影響挺大。相關源碼可以在openjdk的concurrentMarkSweepGeneration.cpp中找到:

void?CMSCollector::collect_in_foreground(bool clear_all_soft_refs, GCCause::Cause cause) {

... ...

switch?(_collectorState) {

case?InitialMarking:

register_foreground_gc_start(cause);

init_mark_was_synchronous = true; // fact to be exploited in re-mark

checkpointRootsInitial(false);

assert(_collectorState == Marking, "Collector state should have changed"

" within checkpointRootsInitial()");

break;

case?Marking:

// initial marking in checkpointRootsInitialWork has been completed

if?(VerifyDuringGC &&

GenCollectedHeap::heap()->total_collections() >= VerifyGCStartAt) {

Universe::verify("Verify before initial mark: ");

}

{

bool res = markFromRoots(false);

assert(res && _collectorState == FinalMarking, "Collector state should "

"have changed");

break;

}

case?FinalMarking:

if?(VerifyDuringGC &&

GenCollectedHeap::heap()->total_collections() >= VerifyGCStartAt) {

Universe::verify("Verify before re-mark: ");

}

checkpointRootsFinal(false, clear_all_soft_refs,

init_mark_was_synchronous);

assert(_collectorState == Sweeping, "Collector state should not "

"have changed within checkpointRootsFinal()");

break;

case?Sweeping:

// final marking in checkpointRootsFinal has been completed

if?(VerifyDuringGC &&

GenCollectedHeap::heap()->total_collections() >= VerifyGCStartAt) {

Universe::verify("Verify before sweep: ");

}

sweep(false);

assert(_collectorState == Resizing, "Incorrect state");

break;

case?Resizing: {

// Sweeping has been completed; the actual resize in this case

// is done separately; nothing to be done in this state.

_collectorState = Resetting;

break;

}

case?Resetting:

// The heap has been resized.

if?(VerifyDuringGC &&

GenCollectedHeap::heap()->total_collections() >= VerifyGCStartAt) {

Universe::verify("Verify before reset: ");

}

save_heap_summary();

reset(false);

assert(_collectorState == Idling, "Collector state should "

"have changed");

break;

case?Precleaning:

case?AbortablePreclean:

// Elide the preclean phase

_collectorState = FinalMarking;

break;

default:

ShouldNotReachHere();

}

}

}

G1下的FullGC

G1或者ParNew+CMS組合前提下,如果真的發(fā)生FullGC,則是單線程完全STW的回收方式(SerialGC),可以想象性能有多差,如果是es,hbase等需要幾十個G的堆,那更是災難。不過JDK10將對其進行優(yōu)化,可以參考:http://openjdk.java.net/jeps/307,如下圖所示:

JEP 307: Parallel Full GC for G1

后面還有一段描述(Description):

The G1 garbage collector is designed to avoid full collections, but when the concurrent collections can't reclaim memory fast enough a fall back full GC will occur. The current implementation of the full GC for G1 uses a single threaded mark-sweep-compact algorithm . We intend to parallelize the mark-sweep-compact algorithm and use the same number of threads as the Young and Mixed collections do. The number of threads can be controlled by the -XX:ParallelGCThreads option, but this will also affect the number of threads used for Young and Mixed collections.

FullGC觸發(fā)條件

這里列舉一些可能導致FullGC的原因,這也是一些高級面試可能問到的問題:

沒有配置 -XX:+DisableExplicitGC情況下System.gc()可能會觸發(fā)FullGC;

Promotion failed;

concurrent mode failure;

Metaspace Space使用達到MaxMetaspaceSize閾值;

執(zhí)行jmap -histo:live或者jmap -dump:live;

說明:統(tǒng)計發(fā)現(xiàn)之前YGC的平均晉升大小比目前old gen剩余的空間大,觸發(fā)的是CMS GC;如果配置了CMS,并且Metaspace Space使用量達到MetaspaceSize閾值也是觸發(fā)CMS GC;

這里可以參考筆者另外一篇文章:?PermSize&MetaspaceSize區(qū)別

執(zhí)行jmap -histo:live觸發(fā)FullGC的gc log如下--關鍵詞?Heap Inspection Initiated GC?,通過 jstat -gccause pid 2s 的LGCC列也能看到同樣的關鍵詞:

[Full GC (Heap Inspection Initiated GC) 2018-03-29T15:26:51.070+0800: 51.754: [CMS: 82418K->55047K(131072K), 0.3246618 secs] 138712K->55047K(249088K), [Metaspace: 60713K->60713K(1103872K)], 0.3249927 secs] [Times: user=0.32 sys=0.01, real=0.32 secs]

執(zhí)行jmap -dump:live觸發(fā)FullGC的gc log如下--關鍵詞?Heap Dump Initiated GC?,通過 jstat -gccause pid 2s 的LGCC列也能看到同樣的關鍵詞:

[Full?GC (Heap?Dump Initiated GC) 2018-03-29T15:31:53.825+0800: 354.510: [CMS2018-03-29T15:31:53.825+0800:?354.510: [CMS:?55047K->56358K(131072K), 0.3116120 secs] 84678K->56358K(249088K), [Metaspace:?62153K->62153K(1105920K)], 0.3119138 secs] [Times:?user=0.31 sys=0.00, real=0.31 secs]

健康的GC

診斷GC的第一步,當然是知道你的JVM的GC是否正常。那么GC是否正常,首先就要看YoungGC,OldGC和FullGC是否正常;無論是定位YoungGC,OldGC,F(xiàn)ullGC哪一種GC,判斷其是否正常主要從兩個維度:?GC頻率和STW時間?;要得到這兩個維度的值,我們需要知道JVM運行了多久,執(zhí)行如下命令即可:

ps?-p pid -o etime

運行結果參考如下,表示這個JVM運行了24天16個小時37分35秒,如果JVM運行時間沒有超過一天,執(zhí)行結果類似這樣"16:37:35":

[afei@ubuntu ~]$ ps -p 11864 -o etime

ELAPSED

24-16:37:35

那么怎樣的GC頻率和STW時間才算是正常呢?這里以我以前開發(fā)過的一個 讀多寫少 的dubbo服務作為參考,該dubbo服務基本情況如下:

日調用量1億+次,接口平均響應時間8ms以內

4個JVM

每個JVM設置Xmx和Xms為4G并且Xmn1G

4核CPU + 8G內存服務器

JDK7

AWS云服務器

GC情況如下圖所示:

GC統(tǒng)計信息

根據這張圖輸出數(shù)據,可以得到如下一些信息:

JVM運行總時間為7293378秒(80*24*3600+9*3600+56*60+18)

YoungGC頻率為2秒/次(7293378/3397184,jstat結果中YGC列的值)

CMS GC頻率為9天/次(因為FGC列的值為18,即?最多?發(fā)生9次CMS GC,所以CMS GC頻率為80/9≈9天/次)

每次YoungGC的時間為6ms(通過YGCT/YGC計算得出)

FullGC幾乎沒有(JVM總計運行80天,F(xiàn)GC才18,即使是18次FullGC,F(xiàn)ullGC頻率也才4.5天/次,更何況實際上FGC=18肯定包含了若干次CMS GC)

如果要清楚的統(tǒng)計CMS GC和FullGC的次數(shù),只能通過GC日志了。

根據上面的GC情況,給個?可參考的健康的GC狀況?:

YoungGC頻率不超過2秒/次;

CMS GC頻率不超過1天/次;

每次YoungGC的時間不超過15ms;

FullGC頻率盡可能完全杜絕;

說明:這里只是參考,不是絕對,不能說這個GC狀況有多好,起碼橫向對比業(yè)務規(guī)模,以及服務器規(guī)格,你的GC狀況不能與筆者的dubbo服務有明顯的差距。

了解GC健康時候的樣子,那么接下來把脈你的JVM GC,看看是有疾在腠理,還是在肌膚,還是在腸胃,甚至已經在骨髓,病入膏肓沒救了;

YGC

YGC是最頻繁發(fā)生的,發(fā)生的概率是OldGC和FullGC的的10倍,100倍,甚至1000倍。同時YoungGC的問題也是最難定位的。這里給出YGC定位三板斧(都是踩過坑):

查看服務器SWAP&IO情況,如果服務器發(fā)生SWAP,會嚴重拖慢GC效率,導致STW時間異常長,拉長接口響應時間,從而影響用戶體驗(推薦神器 sar ,yum install sysstat即可,想了解該命令,請搜索" linux sar ");

查看StringTable情況(請參考:?探索StringTable提升YGC性能?)

排查每次YGC后幸存對象大小(JVM模型基于分配的對象朝生夕死的假設設計,如果每次YGC后幸存對象較大,可能存在問題)

未完待續(xù)……(可以在留言中分享你的idea)

排查每次YGC后幸存對象大小可通過GC日志中發(fā)生YGC的日志計算得出,例如下面兩行GC日志,第二次YGC相比第一次YGC,整個Heap并沒有增長(都是647K),說明回收效果非常理想:

2017-11-28T10:22:57.332+0800: [GC (Allocation Failure) 2017-11-28T10:22:57.332+0800: [ParNew: 7974K->0K(9216K), 0.0016636 secs] 7974K->647K(19456K), 0.0016865?secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

2017-11-28T10:22:57.334+0800: [GC (Allocation Failure) 2017-11-28T10:22:57.334+0800: [ParNew: 7318K->0K(9216K), 0.0002355 secs] 7965K->647K(19456K), 0.0002742?secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

再看下面兩行GC日志,第二次YGC相比第一次YGC,整個Heap從2707K增長到了4743K,說明回收效果不太理想,如果每次YGC時發(fā)現(xiàn)好幾十M甚至上百M的對象幸存,那么可能需要著手排查了:

2017-11-28T10:26:41.890+0800: [GC (Allocation Failure) 2017-11-28T10:26:41.890+0800: [ParNew: 7783K->657K(9216K), 0.0013021 secs] 7783K->2707K(19456K), 0.0013416?secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

2017-11-28T10:26:41.892+0800: [GC (Allocation Failure) 2017-11-28T10:26:41.892+0800: [ParNew: 7982K->0K(9216K), 0.0018354 secs] 10032K->4743K(19456K), 0.0018536?secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

可參考的健康的GC狀況給出建議YGC頻率不超過2秒/次,經驗值2秒~10秒/次都是比較合理的YGC頻率;

如果YGC頻率遠高于這個值,例如20秒/次,30秒/次,甚至60秒/次,這種情況下,說明JVM相當空閑,處于基本上無事可做的狀態(tài)。建議縮容,減少服務器浪費;

如果YoungGC頻率遠低于這個值,例如1秒/次,甚至1秒/好多次,這種情況下,JVM相當繁忙,建議follow如下步驟進行初步癥斷:

檢查Young區(qū),Young區(qū)在整個堆占比在25%~40%比較合理,如果Young區(qū)太小,建議擴大Xmn。

檢查SurvivorRatio,保持默認值8即可,Eden:S0:S1=8:1:1是一個比較合理的值;

OldGC

上面已經提及:到目前為止HotSpot JVM虛擬機只單獨回收Old區(qū)的只有CMS GC。觸發(fā)CMS GC條件比較簡單,JVM有一個線程定時掃描Old區(qū),時間間隔可以通過參數(shù)?-XX:CMSWaitDuration?設置(默認就是2s),掃描發(fā)現(xiàn)Old區(qū)占比超過參數(shù)?-XX:CMSInitiatingOccupancyFraction設定值(CMS條件下默認為68%),就會觸發(fā)CMS GC。建議搭配?-XX:+UseCMSInitiatingOccupancyOnly?參數(shù)使用,簡化CMS GC觸發(fā)條件,?只有?在Old區(qū)占比滿足?-XX:CMSInitiatingOccupancyFraction?條件的情況下才觸發(fā)CMS GC;

可參考的健康的GC狀況給出建議CMS GC頻率不超過1天/次,如果CMS GC頻率1天發(fā)生數(shù)次,甚至上10次,說明你的GC情況病的不輕了,建議follow如下步驟進行初步癥斷:

檢查Young區(qū)與Old區(qū)比值,盡量留60%以上的堆空間給Old區(qū);

通過jstat查看每次YoungGC后晉升到Old區(qū)對象占比,如果發(fā)現(xiàn)每次YoungGC后Old區(qū)漲好幾個百分點,甚至上10個點,說明有大對象,建議dump(參考 jmap -dump:format=b,file=app.bin pid )后用MAT分析;

如果不停的CMS GC,Old區(qū)降不下去,建議先執(zhí)行 jmap -histo pid | head -n10 查看TOP10對象分布,如果除了 [B和[C ,即byte[]和char[],還有其他占比較大的實例,如下圖所示中排名第一的Object數(shù)組,也可通過dump后用MAT分析問題;

如果TOP10對象中有?StandartSession?對象,排查你的業(yè)務代碼中有沒有顯示使用?HttpSession?,例如 String id = request.getSession().getId(); ,一般的OLTP系統(tǒng)都是無狀態(tài)的,幾乎不會使用?HttpSession?,且?HttpSession?的的生命周期很長,會加快Old區(qū)增長速度;

異常的大對象.png

筆者曾經幫一位朋友排查過一個問題:他也是TOP對象中有?StandartSession?對象,并且占比較大,后面讓他排查發(fā)現(xiàn)在接口中使用了HttpSession生成一個唯一ID,讓他改成用UUID就解決了OldGC頻繁的問題。

FullGC

如果配置CMS,由于CMS采用標記清理算法,會有內存碎片的問題,推薦配置一個查看內存碎片程度的JVM參數(shù):?PrintFLSStatistics?。

如果配置ParallelOldGC,那么每次Old區(qū)滿后,會觸發(fā)FullGC,如果FullGC頻率過高,也可以通過上面?OldGC?提及的排查方法;

如果沒有配置 -XX:+DisableExplicitGC ,即沒有屏蔽 System.gc() 觸發(fā)FullGC,那么可以通過排查GC日志中有System字樣判斷是否由System.gc()觸發(fā),日志樣本如下:

558082.666: [Full GC (System) [PSYoungGen: 368K->0K(42112K)] [PSOldGen: 36485K->32282K(87424K)] 36853K->32282K(129536K) [PSPermGen: 34270K->34252K(196608K)], 0.2997530?secs]

或者通過 jstat -gccause pid 2s pid 判定,?LGCC?表示最近一次GC原因,如果為?System.gc?,表示由System.gc()觸發(fā),?GCC?表示當前GC原因,如果當前沒有GC,那么就是No GC:

System.gc引起的FullGC

如果你現(xiàn)在在JAVA這條路上掙扎,也想在IT行業(yè)拿高薪,可以參加我們的訓練營課程,選擇最適合自己的課程學習,技術大牛親授,8個月后,進入名企拿高薪。我們的課程內容有:Java工程化、高性能及分布式、高性能、高架構、性能調優(yōu)、Spring,MyBatis,Netty源碼分析和大數(shù)據等多個知識點。如果你想拿高薪的,想學習的,想就業(yè)前景好的,想跟別人競爭能取得優(yōu)勢的,想進阿里面試但擔心面試不過的,你都可以來,q群號為:835638062

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容