JVM基礎(chǔ)和問題分析入門筆記

1.1 JDK跃脊、JRE缀皱、JVM的關(guān)系

JDK是java開發(fā)工具集合崎坊,JRE是java運行環(huán)境,JVM是Java虛擬機

JDK > JRE > JVM

JDK = JRE + 開發(fā)工具

JRE = JVM + 類庫

三者在開發(fā)運行Java程序時的交互關(guān)系:

通過JDK開發(fā)的程序趁仙,編譯以后,可以打包發(fā)給裝有JRE的機器上去運行垦页。而運行的程序雀费,則是通過Java命令啟動的一個JVM實例,代碼邏輯的執(zhí)行都運行在這個JVM實例上痊焊。

Java程序的開發(fā)運行過程:

利用JDK開發(fā)Java程序盏袄,編譯成字節(jié)碼或者打包程序。然后在JRE里啟動一個JVM實例宋光,加載貌矿、驗證、執(zhí)行Java字節(jié)碼和依賴庫罪佳,運行Java程序逛漫。JVM將程序和依賴庫的Java字節(jié)碼解析并變成本地代碼執(zhí)行,產(chǎn)生結(jié)果赘艳。

常用性能指標

  1. 延遲:平均響應(yīng)時間
  2. 吞吐量:每秒處理事務(wù)數(shù)TPS酌毡,每秒處理請求數(shù)QPS
  3. 系統(tǒng)容量:設(shè)計容量,硬件配置蕾管,成本約束

這三個維度互相關(guān)聯(lián)枷踏,相互制約。

我們可采用的手段和方式包括:

  • 使用 JDWP 或開發(fā)工具做本地/遠程調(diào)試
  • 系統(tǒng)和 JVM 的狀態(tài)監(jiān)控掰曾,收集分析指標
  • 性能分析: CPU 使用情況/內(nèi)存分配分析
  • 內(nèi)存分析: Dump 分析/GC 日志分析
  • 調(diào)整 JVM 啟動參數(shù)旭蠕,GC 策略等等

性能調(diào)優(yōu)總結(jié)

性能調(diào)優(yōu)的第一步是制定指標,收集數(shù)據(jù)旷坦,第二步是找瓶頸掏熬,然后分析解決瓶頸問題。通過這些手段秒梅,找當前的性能極限值旗芬。壓測調(diào)優(yōu)到不能再優(yōu)化了的 TPS 和 QPS,就是極限值捆蜀。知道了極限值疮丛,我們就可以按業(yè)務(wù)發(fā)展測算流量和系統(tǒng)壓力幔嫂,以此做容量規(guī)劃,準備機器資源和預期的擴容計劃誊薄。最后在系統(tǒng)的日常運行過程中履恩,持續(xù)觀察,逐步重做和調(diào)整以上步驟暇屋,長期改善改進系統(tǒng)性能似袁。

脫離場景談性能都是耍流氓”,實際的性能分析調(diào)優(yōu)過程中咐刨,我們需要根據(jù)具體的業(yè)務(wù)場景昙衅,綜合考慮成本和性能,使用最合適的辦法去處理定鸟。系統(tǒng)的性能優(yōu)化到 3000TPS 如果已經(jīng)可以在成本可以承受的范圍內(nèi)滿足業(yè)務(wù)發(fā)展的需求而涉,那么再花幾個人月優(yōu)化到 3100TPS 就沒有什么意義,同樣地如果花一倍成本去優(yōu)化到 5000TPS 也沒有意義联予。

過早的優(yōu)化是萬惡之源”啼县,我們需要考慮在恰當?shù)臅r機去優(yōu)化系統(tǒng)。在業(yè)務(wù)發(fā)展的早期沸久,量不大季眷,性能沒那么重要。我們做一個新系統(tǒng)卷胯,先考慮整體設(shè)計是不是 OK子刮,功能實現(xiàn)是不是 OK,然后基本的功能都做得差不多的時候(當然整體的框架是不是滿足性能基準窑睁,可能需要在做項目的準備階段就通過 POC(概念證明)階段驗證挺峡。),最后再考慮性能的優(yōu)化工作担钮。因為如果一開始就考慮優(yōu)化橱赠,就可能要想太多導致過度設(shè)計了。而且主體框架和功能完成之前箫津,可能會有比較大的改動狭姨,一旦提前做了優(yōu)化,可能這些改動導致原來的優(yōu)化都失效了苏遥,又要重新優(yōu)化送挑,多做了很多無用功。

關(guān)于跨平臺

  • 編譯執(zhí)行:C暖眼,C++,Golang纺裁,Rust诫肠,C#司澎,Java,Scala栋豫,Clojure挤安,Kotlin,Swift 等等
  • 解釋執(zhí)行:NodeJS丧鸯,Python蛤铜,Perl,Ruby和JavaScript 的部分實現(xiàn)等等

一般來說解釋型語言都是跨平臺的丛肢,同一份腳本代碼围肥,可以由不同平臺上的解釋器解釋執(zhí)行。

但是對于編譯型語言蜂怎,存在兩種級別的跨平臺: 源碼跨平臺和二進制跨平臺穆刻。

1、典型的源碼跨平臺(C++):

<img src="https://tva1.sinaimg.cn/large/e6c9d24ely1h4gmwl1dy2j21160n4gna.jpg" alt="71212109.png" style="zoom:50%;" />

2杠步、典型的二進制跨平臺(Java 字節(jié)碼):

<img src="https://tva1.sinaimg.cn/large/e6c9d24ely1h4gmws1yf3j21460kwgnc.jpg" alt="71237637.png" style="zoom:50%;" />

C++可以一次編寫氢伟,到處編譯,但是在不同環(huán)境的依賴不一致或者不完全幽歼,需要到處調(diào)試朵锣,到處找依賴,該配置甸私。

Java通過虛擬機技術(shù)解決了這個問題诚些。源碼只需要編譯一次,然后把編譯后的 class 文件或 jar 包颠蕴,部署到不同平臺泣刹,就可以直接通過安裝在這些系統(tǒng)中的 JVM 上面執(zhí)行。 同時可以把依賴庫(jar 文件)一起復制到目標機器犀被,慢慢地又有了可以在各個平臺都直接使用的 Maven 中央庫(類似于 linux 里的 yum 或 apt-get 源椅您,macos 里的 homebrew,現(xiàn)代的各種編程語言一般都有了這種包依賴管理機制:python 的 pip寡键,dotnet 的 nuget掀泳,NodeJS 的 npm,golang 的 dep西轩,rust 的 cargo 等等)员舵。這樣就實現(xiàn)了讓同一個應(yīng)用程序在不同的平臺上直接運行的能力。

JAVA字節(jié)碼

為什么要學

Java 中的字節(jié)碼藕畔,英文名為 bytecode, 是 Java 代碼編譯后的中間代碼格式马僻。JVM 需要讀取并解析字節(jié)碼才能執(zhí)行相應(yīng)的任務(wù)。

了解字節(jié)碼對于編寫高性能代碼至關(guān)重要注服。通過修改字節(jié)碼來調(diào)整程序的行為是司空見慣的事情韭邓。想了解分析器(Profiler)措近,Mock 框架,AOP 等工具和技術(shù)這一類工具女淑,則必須完全了解 Java 字節(jié)碼瞭郑。

簡介

有一件有趣的事情,就如名稱所示, Java bytecode 由單字節(jié)(byte)的指令組成鸭你,理論上最多支持 256 個操作碼(opcode)屈张。實際上 Java 只使用了 200 左右的操作碼, 還有一些操作碼則保留給調(diào)試操作袱巨。

操作碼阁谆, 下面稱為 指令, 主要由類型前綴操作名稱兩部分組成。

例如瓣窄,'i' 前綴代表 ‘integer’笛厦,所以,'iadd' 很容易理解, 表示對整數(shù)執(zhí)行加法運算俺夕。

根據(jù)指令的性質(zhì)裳凸,主要分為四個大類:

  1. 棧操作指令,包括與局部變量交互的指令
  2. 程序流程控制指令
  3. 對象操作指令劝贸,包括方法調(diào)用指令
  4. 算術(shù)運算以及類型轉(zhuǎn)換指令

此外還有一些執(zhí)行專門任務(wù)的指令姨谷,比如同步(synchronization)指令,以及拋出異常相關(guān)的指令等等映九。下文會對這些指令進行詳細的講解梦湘。

獲取字節(jié)碼清單

可以用 **javap** 工具來獲取 class 文件中的指令清單。 **javap**是標準 JDK 內(nèi)置的一款工具, 專門用于反編譯 class 文件件甥。

GC

Serial GC 日志解讀

我們關(guān)注的主要是兩個數(shù)據(jù):GC 暫停時間捌议,以及 GC 之后的內(nèi)存使用量/使用率。

FullGC引有,我們主要關(guān)注 GC 之后內(nèi)存使用量是否下降瓣颅,其次關(guān)注暫停時間。簡單估算譬正,GC 后老年代使用量為 220MB 左右宫补,耗時 50ms。如果內(nèi)存擴大 10 倍曾我,GC 后老年代內(nèi)存使用量也擴大 10 倍粉怕,那耗時可能就是 500ms 甚至更高,就會系統(tǒng)有很明顯的影響了抒巢。這也是我們說串行 GC 性能弱的一個原因贫贝,服務(wù)端一般是不會采用串行 GC 的。

Tenured:用于清理老年代空間的垃圾收集器名稱蛉谜。Tenured 表明使用的是單線程的 STW 垃圾收集器稚晚,使用的算法為“標記—清除—整理(mark-sweep-compact)”凤优。

[Times: user=0.05 sys=0.00,real=0.05 secs]:GC 事件的持續(xù)時間蜈彼,分為 user、sys俺驶、real 三個部分幸逆。因為串行垃圾收集器只使用單個線程,因此“real=user+system”暮现。50 毫秒的暫停時間还绘,比起前面年輕代的 GC 來說增加了一倍左右。這個時間跟什么有關(guān)系呢栖袋?答案是:GC 時間拍顷,與 GC 后存活對象的總數(shù)量關(guān)系最大。

Parallel GC 日志解讀

并行垃圾收集器對年輕代使用“標記—復制(mark-copy)”算法塘幅,對老年代使用“標記—清除—整理(mark-sweep-compact)”算法昔案。

年輕代和老年代的垃圾回收時都會觸發(fā) STW 事件,暫停所有的應(yīng)用線程电媳,再來執(zhí)行垃圾收集踏揣。在執(zhí)行“標記”和“復制/整理”階段時都使用多個線程,因此得名“Parallel”匾乓。

通過多個 GC 線程并行執(zhí)行的方式捞稿,能使 JVM 在多 CPU 平臺上的 GC 時間大幅減少。

通過命令行參數(shù) -XX:ParallelGCThreads=NNN 可以指定 GC 線程的數(shù)量拼缝,其默認值為 CPU 內(nèi)核數(shù)量娱局。

并行垃圾收集器適用于多核服務(wù)器,其主要目標是增加系統(tǒng)吞吐量(也就是降低 GC 總體消耗的時間)咧七。為了達成這個目標衰齐,會使用盡可能多的 CPU 資源:

  • 在 GC 事件執(zhí)行期間,所有 CPU 內(nèi)核都在并行地清理垃圾猪叙,所以暫停時間相對來說更短娇斩;
  • 在兩次 GC 事件中間的間隔期,不會啟動 GC 線程穴翩,所以這段時間內(nèi)不會消耗任何系統(tǒng)資源犬第。

另一方面,因為并行 GC 的所有階段都不能中斷芒帕,所以并行 GC 很可能會出現(xiàn)長時間的卡頓歉嗓。

長時間卡頓的意思,就是并行 GC 啟動后背蟆,一次性完成所有的 GC 操作鉴分,所以單次暫停的時間較長哮幢。

假如系統(tǒng)延遲是非常重要的性能指標,那么就應(yīng)該選擇其他垃圾收集器志珍。

Minor GC 日志分析

前面的 GC 事件是發(fā)生在年輕代 Minor GC:

2019-12-18T00:37:47.463-0800: 0.690:
  [GC (Allocation Failure)
    [PSYoungGen: 104179K->14341K(116736K)]
    383933K->341556K(466432K)橙垢,0.0229343 secs]
  [Times: user=0.04 sys=0.08,real=0.02 secs]

解讀如下:

  1. 2019-12-18T00:37:47.463-0800: 0.690:GC 事件開始的時間伦糯。
  2. GC:用來區(qū)分 Minor GC 還是 Full GC 的標志柜某。這里是一次“小型 GC(Minor GC)”。
  3. PSYoungGen:垃圾收集器的名稱敛纲。這個名字表示的是在年輕代中使用并行的“標記—復制(mark-copy)”喂击,全線暫停(STW)垃圾收集器。104179K->14341K(116736K) 表示 GC 前后的年輕代使用量淤翔,以及年輕代的總大小翰绊,簡單計算 GC 后的年輕代使用率 14341K/116736K=12%。
  4. 383933K->341556K(466432K) 則是 GC 前后整個堆內(nèi)存的使用量旁壮,以及此時可用堆的總大小监嗜,GC 后堆內(nèi)存使用率為 341556K/466432K=73%,這個比例不低寡具,事實上前面已經(jīng)發(fā)生過 FullGC 了秤茅,只是這里沒有列出來。
  5. [Times: user=0.04 sys=0.08童叠,real=0.02 secs]:GC 事件的持續(xù)時間框喳,通過三個部分來衡量。user 表示 GC 線程所消耗的總 CPU 時間厦坛,sys 表示操作系統(tǒng)調(diào)用和系統(tǒng)等待事件所消耗的時間五垮; real 則表示應(yīng)用程序?qū)嶋H暫停的時間。因為并不是所有的操作過程都能全部并行杜秸,所以在 Parallel GC 中放仗,real 約等于 user+system/GC 線程數(shù)。筆者的機器是 8 個物理線程撬碟,所以默認是 8 個 GC 線程诞挨。分析這個時間,可以發(fā)現(xiàn)呢蛤,如果使用串行 GC惶傻,可能得暫停 120 毫秒,但并行 GC 只暫停了 20 毫秒其障,實際上性能是大幅度提升了银室。

通過這部分日志可以簡單算出:在 GC 之前,堆內(nèi)存總使用量為 383933K,其中年輕代為 104179K蜈敢,那么可以算出老年代使用量為 279754K辜荠。

在此次 GC 完成后,年輕代使用量減少了 104179K-14341K=89838K抓狭,總的堆內(nèi)存使用量減少了 383933K-341556K=42377K伯病。

那么我們可以計算出有“89838K-42377K=47461K”的對象從年輕代提升到老年代。老年代的使用量為:341556K-14341K=327215K否过。

老年代的大小為 466432K-116736K=349696K狱从,使用率為 327215K/349696K=93%,基本上快滿了叠纹。

總結(jié):

年輕代 GC,我們可以關(guān)注暫停時間敞葛,以及 GC 后的內(nèi)存使用率是否正常誉察,但不用特別關(guān)注 GC 前的使用量,而且只要業(yè)務(wù)在運行惹谐,年輕代的對象分配就少不了持偏,回收量也就不會少。

此次 GC 的內(nèi)存變化示意圖為:

Full GC 日志分析

前面介紹了并行 GC 清理年輕代的 GC 日志氨肌,下面來看看清理整個堆內(nèi)存的 GC 日志:

2019-12-18T00:37:47.486-0800: 0.713:
  [Full GC (Ergonomics)
    [PSYoungGen: 14341K->0K(116736K)]
    [ParOldGen: 327214K->242340K(349696K)]
    341556K->242340K(466432K)鸿秆,
    [Metaspace: 3322K->3322K(1056768K)],
  0.0656553 secs]
  [Times: user=0.30 sys=0.02怎囚,real=0.07 secs]

解讀一下:

  1. 2019-12-18T00:37:47.486-0800:GC 事件開始的時間卿叽。
  2. Full GC:完全 GC 的標志。Full GC 表明本次 GC 清理年輕代和老年代恳守,Ergonomics 是觸發(fā) GC 的原因考婴,表示 JVM 內(nèi)部環(huán)境認為此時可以進行一次垃圾收集。
  3. [PSYoungGen: 14341K->0K(116736K)]:和上面的示例一樣催烘,清理年輕代的垃圾收集器是名為“PSYoungGen”的 STW 收集器沥阱,采用“標記—復制(mark-copy)”算法。年輕代使用量從 14341K 變?yōu)?0伊群,一般 Full GC 中年輕代的結(jié)果都是這樣考杉。
  4. ParOldGen:用于清理老年代空間的垃圾收集器類型。在這里使用的是名為 ParOldGen 的垃圾收集器舰始,這是一款并行 STW 垃圾收集器崇棠,算法為“標記—清除—整理(mark-sweep-compact)”。327214K->242340K(349696K)]:在 GC 前后老年代內(nèi)存的使用情況以及老年代空間大小蔽午。簡單計算一下易茬,GC 之前,老年代使用率為 327214K/349696K=93%,GC 后老年代使用率 242340K/349696K=69%抽莱,確實回收了不少范抓。那么有多少內(nèi)存提升到老年代呢?其實在 Full GC 里面不好算食铐,而在 Minor GC 之中比較好算匕垫,原因大家自己想一想。
  5. 341556K->242340K(466432K):在垃圾收集之前和之后堆內(nèi)存的使用情況虐呻,以及可用堆內(nèi)存的總?cè)萘肯蟊谩:唵畏治隹芍珿C 之前堆內(nèi)存使用率為 341556K/466432K=73%斟叼,GC 之后堆內(nèi)存的使用率為:242340K/466432K=52%偶惠。
  6. [Metaspace: 3322K->3322K(1056768K)]:前面我們也看到了關(guān)于 Metaspace 空間的類似信息±噬可以看出忽孽,在 GC 事件中 Metaspace 里面沒有回收任何對象。
  7. 0.0656553secs:GC 事件持續(xù)的時間谢床,以秒為單位兄一。
  8. [Times: user=0.30 sys=0.02,real=0.07 secs]:GC 事件的持續(xù)時間识腿,含義參見前面出革。

Full GC 和 Minor GC 的區(qū)別是很明顯的,此次 GC 事件除了處理年輕代渡讼,還清理了老年代和 Metaspace骂束。

總結(jié):

Full GC 時我們更關(guān)注老年代的使用量有沒有下降,以及下降了多少成箫。如果 FullGC 之后內(nèi)存不怎么下降栖雾,使用率還很高,那就說明系統(tǒng)有問題了伟众。

此次 GC 的內(nèi)存變化示意圖為:

細心的同學可能會發(fā)現(xiàn)析藕,此次 FullGC 事件和前一次 MinorGC 事件是緊挨著的:0.690+0.02secs~0.713。因為 Minor GC 之后老年代使用量達到了 93%凳厢,所以接著就觸發(fā)了 Full GC账胧。

內(nèi)存計算

操作系統(tǒng)中的最大可用內(nèi)存除去操作系統(tǒng)本身使用的部分,剩下的都可以為某一個進程服務(wù)先紫,在JVM進程中治泥,內(nèi)存又被分為堆、本地內(nèi)存和棧等三大塊遮精,Java堆是JVM自動管理的內(nèi)存居夹,應(yīng)用的對象的創(chuàng)建和銷毀败潦、類的裝載等都發(fā)生在這里,本地內(nèi)存是Java應(yīng)用使用的一種特殊內(nèi)存准脂,JVM并不直接管理其生命周期劫扒,每個線程也會有一個棧,是用來存儲線程工作過程中產(chǎn)生的方法局部變量狸膏、方法參數(shù)和返回值的沟饥,每個線程對應(yīng)的棧的默認大小為1M。

從內(nèi)存角度來看創(chuàng)建線程需要內(nèi)存空間湾戳,如果JVM進程正當一個應(yīng)用創(chuàng)建線程贤旷,而操作系統(tǒng)沒有剩余的內(nèi)存分配給此JVM進程,則會拋出問題中的OOM異常:unable to create new native thread砾脑。

如下公式可以用來從內(nèi)存角度計算允許創(chuàng)建的最大線程數(shù):

最大線程數(shù) = (操作系統(tǒng)最大可用內(nèi)存 - JVM內(nèi)存 - 操作系統(tǒng)預留內(nèi)存)/ 線程棧大小

根據(jù)這個公式幼驶,我們可以通過剩余內(nèi)存計算可以創(chuàng)建線程的數(shù)量。

使用free -m查看剩余內(nèi)存

使用ulimit -a來顯示當前的各種系統(tǒng)對用戶使用資源的限制:

max user processes        (-u) 1024

機器設(shè)置的允許使用的最大用戶進程數(shù)為1024韧衣。

使用jstack命令查看Java棧

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末县遣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子汹族,更是在濱河造成了極大的恐慌,老刑警劉巖其兴,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件顶瞒,死亡現(xiàn)場離奇詭異,居然都是意外死亡元旬,警方通過查閱死者的電腦和手機榴徐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匀归,“玉大人坑资,你說我怎么就攤上這事∧露耍” “怎么了袱贮?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長体啰。 經(jīng)常有香客問我攒巍,道長,這世上最難降的妖魔是什么荒勇? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上棚辽,老公的妹妹穿的比我還像新娘浇坐。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布跨蟹。 她就那樣靜靜地躺著雳殊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喷市。 梳的紋絲不亂的頭發(fā)上相种,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音品姓,去河邊找鬼寝并。 笑死,一個胖子當著我的面吹牛腹备,可吹牛的內(nèi)容都是我干的衬潦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼植酥,長吁一口氣:“原來是場噩夢啊……” “哼镀岛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起友驮,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤漂羊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后卸留,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體走越,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年耻瑟,在試婚紗的時候發(fā)現(xiàn)自己被綠了旨指。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡喳整,死狀恐怖谆构,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情框都,我是刑警寧澤搬素,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站魏保,受9級特大地震影響蔗蹋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜囱淋,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一猪杭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧妥衣,春花似錦皂吮、人聲如沸戒傻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽需纳。三九已至,卻和暖如春艺挪,著一層夾襖步出監(jiān)牢的瞬間不翩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工麻裳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留口蝠,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓津坑,卻偏偏與公主長得像妙蔗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子疆瑰,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

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