Java进程高CPU使用调查方法简介

介绍

关于java进程占用cpu问题,一般都是想知道

  1. 哪个(些)线程使用了更多的cpu
  2. 哪个(些)方法使用了更多的cpu

本文给出一个非常简要的介绍并给出了各个方法的使用例子,更深入使用可参考相关文档。 我觉得这些方法简单实用,已经帮助我解决过几次问题(产品环境过慢,性能调优),希望对你也有帮助。

简要总结

目标 解决方式 说明 适用于
哪个(些)线程占用了很多的cpu Step 1. 通过ps命令得到jvm的各个线程的cpu使用率(并排序)
Step 2. 通过jstack的输出,找到这个线程(Step1中各个线程都有线程号)
注意:Step1(ps)中线程号为10进制,Step2(jstack)中为16进制。人肉转换的话,可以使用在线工具 http://www.binaryhexconverter.com/hex-to-decimal-converter, 或者 printf "%x\n" decimalNumber 来获得16进制
本方法非常实用,而且被讨论过很多次(如,杨彪 一次线上游戏卡死的解决历程 2017, http://www.boxjar.com/using-top-and-jstack-to-find-the-java-thread-that-is-hogging-the-cpu/ 2013)。建议直接使用现成脚本(譬如:https://github.com/robertleepeak/wizard-scripts/blob/master/show-busiest-java-threads 知名架构师李艳鹏的github).
本方法非常适用于生产环境出现的cpu使用过高导致程序缓慢的问题。
下面的例子中也提供了一个可以直接复制/粘贴使用的例子。
产品环境
开发/测试环境
哪个(些)方法占用了很多的cpu - via jvisualvm visualvm连接java 进程后
1. 点击Sample 标签
2. 开始你的测试,然后马上点击cpu按钮
3. cpu占用测试结束后,点击 Stop
4. 点击Snapshot暂存结果
5. 观察分析Snapshot结果
6. Snapshot结果可以保存
下面例子中,提供了截屏。使用方式比较简单直接。
另外:visualvm还支持使用类似的方式查看内存在检测期间的分配情况
ONLY 开发/测试环境
因为一般不允许通过visualvm连接产品环境。
哪个(些)方法占用了很多的cpu - via hprof 官方链接(java8) 添加-agentlib:hprof=cpu=samples到你的java进程启动脚本中,开始测试。测试结束后,关闭java进程。java进程退出后,在当前目录形成一个文件java.hprof.txt。这个文件中,包含了cpu使用方法排名。
本文会提供一个例子输出供参考。
该输出并没有visualvm信息丰富、直观。只有在visualvm无法使用,或者必须background执行的时候,才推荐使用。
None
平时工作中,我几乎从来没用使用这种方法。因为开发环境中,visualvm更合适。生产环境中,hprof是不会被使用的。
这里列出来只是提供一个新的思路。

note: https://www.tablesgenerator.com/markdown_tables# could help to generate markdown table
note: for newline in markdown table, you have to input

特别例子 - 线程 thread.sleep, or readBytes from console,但是仍然CPU很高

E.g. High CPU while read lines from console: http://www.jianshu.com/p/4a6fe6c82311
E.g. 微信群中,有人提出在一台测试Tomcat(没有任何load),但是cpu仍使用了9%左右。通过jstack发现cpu最高的线程stack为Thread.sleep
此类问题相对来说更复杂一些,因为我们已经找到了相关的繁忙代码,但是这类代码不应该造成cpu繁忙。如read from console应该永远都不到,因为没有人输入; 如 thread.sleep不应该造成high cpu。

这里我给出一个思路

  1. 使用jvisualvm继续看一下,能否找到繁忙method的具体情况。可能在这一步就找到了问题根本原因
  2. 查看相关源代码

如:read lines from console这个例子,通过查看源代码可知,我们不期望从console读到任何东西,那么为什么读到了?读到了什么值? 可以通过重新编译源代码或者btrace注入的方式,看一下到底读到的什么值,进一步查看。那篇文章中,作者直接在console read之后,加入了一个sleep,简单粗暴的解决了这个问题。其实也没啥问题。

如:tomcat sleep的例子,通过检查源代码,

at java.lang.Thread.sleep(NativeMethod)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java: 1355)
at java.lang.Thread.run(Thread.java:748)

如果真正的在sleep,cpu肯定不会高。我怀疑下面的backgroundProcessorDelay值有问题。

    while (!threadDone) {
        try {

            Thread.sleep(backgroundProcessorDelay * 1000L);
        } catch (InterruptedException e) {
            // Ignore
        }
        
        if (!threadDone) {
            processChildren(ContainerBase.this);
        }

    }

这个可以通过btrace注入Thread.sleep方法,查看它到底调用的值是多少。

例子:查看哪个线程最繁忙(下面脚本可以直接复制粘贴到你的linux shell中)

直接复制下面的几行,就可以得到高cpu的那些线程,以及一个完整的jstack输出文件

process_id=YOUR_PROCESS_ID_TO_BE_REPLACED
tmp_jstack_file=/tmp/$(date '+%Y%m%d_%H%M%S')_$USER_$$_${process_id}_jstack_$RANDOM
jstack ${process_id} > $tmp_jstack_file

ps -Leo pid,lwp,user,comm,pcpu --no-headers| grep "^[[:space:]]\+${process_id}[[:space:]]\+" | sort -k5 -r -n | head -n"${top_count:-5}" |  while read thread_line 
do
    threadId=$(echo ${thread_line} | awk '{print $2}')
    threadId0x=$(printf %x ${threadId})     
    java_thread_info=$(grep "[[:space:]]nid=0x${threadId0x}[[:space:]]" ${tmp_jstack_file})
    echo ${thread_line} ${java_thread_info}
done

echo "pid lwp user comm pcpu java_thread_info <- above columns"
echo "see full jstack info here: $tmp_jstack_file"

这是我的一个例子输出

wby@BaoyingUbuntu:~$ process_id=1702
wby@BaoyingUbuntu:~$ ps -Leo pid,lwp,user,comm,pcpu --no-headers| grep "^[[:space:]]\+${process_id}[[:space:]]\+" | sort -k5 -r -n | head -n"${top_count:-5}" |  while read thread_line
> do
> threadId=$(echo ${thread_line} | awk '{print $2}')
> threadId0x=$(printf %x ${threadId})
> java_thread_info=$(grep "[[:space:]]nid=0x${threadId0x}[[:space:]]" ${tmp_jstack_file})
> echo ${thread_line} ${java_thread_info}
> done
1702 1709 wby java 1.0 "HPROF cpu sampling thread" #6 daemon prio=10 os_prio=0 tid=0x00007fdb70123800 nid=0x6ad runnable [0x0000000000000000]
1702 1704 wby java 1.0 "VM Thread" os_prio=0 tid=0x00007fdb700b1800 nid=0x6a8 runnable
1702 1756 wby java 0.0 "Attach Listener" #14 daemon prio=9 os_prio=0 tid=0x00007fdb48002000 nid=0x6dc waiting on condition [0x0000000000000000]
1702 1716 wby java 0.0 "VM Periodic Task Thread" os_prio=0 tid=0x00007fdb70247000 nid=0x6b4 waiting on condition
1702 1715 wby java 0.0 "RMI TCP Accept-0" #13 daemon prio=5 os_prio=0 tid=0x00007fdb70241800 nid=0x6b3 runnable [0x00007fdb5e0f6000]
wby@BaoyingUbuntu:~$ echo "pid lwp user comm pcpu java_thread_info <- above columns"
pid lwp user comm pcpu java_thread_info <- above columns
wby@BaoyingUbuntu:~$ echo "see full jstack info here: $tmp_jstack_file"
see full jstack info here: /tmp/20171023_084044_1678_1702_jstack_1280

在上面例子输出中,我们可以看到"HPROF cpu sampling thread"占用了最多的cpu(1.0)

1702 1709 wby java 1.0 "HPROF cpu sampling thread" #6 daemon prio=10 os_prio=0 tid=0x00007fdb70123800 nid=0x6ad runnable [0x0000000000000000]

例子:通过visualvm定位各个方法占用cpu时间

你可以同本地电脑的visualvm(JDK_HOME/bin/jvisualvm.exe),连接查看远程服务器上java进程的情况(cpu, thread, memory, 等)。这里将举例介绍如何通过visualvm查看java进程的各个方法使用的cpu情况

如果是本地的java进程,那么启动visualvm之后,可以直接从左侧的Local列表中找到。
如果是远程的java进程,请确保远程进程打开了相关的端口如

-Dcom.sun.management.jmxremote.port=3333  -Dcom.sun.management.jmxremote.ssl=false  -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=远端服务器ip

然后添加JMX链接 - 远端服务器ip:远端端口 (即3333, 如果-Dcom.sun.management.jmxremote.port=3333)


cpu_jvisualvm.simple.png

接下来的截图,通过sample更多的package,得到更丰富的结果。


cpu_jvisualvm.setup.profile.package.png

cpu_jvisualvm.setup.profile.package.output.png

例子:通过hprof定位各个方法占用cpu时间

java -cp ../classes -agentlib:hprof=cpu=samples baoying.memory.HeapEater
hprof支持更多的功能和参数,详见官方链接(java8)

平时工作中,我几乎从来没用使用这种方法。因为开发环境中,visualvm更合适。生产环境中,hprof是不会被使用的。
这里列出来只是提供一个新的思路。

--------
THREAD START (obj=50000437, id = 200004, name="RMI TCP Accept-0", group="system")
THREAD START (obj=50000437, id = 200005, name="RMI TCP Accept-3333", group="system")
THREAD START (obj=50000437, id = 200006, name="RMI TCP Accept-0", group="system")
THREAD START (obj=50000437, id = 200001, name="main", group="main")
THREAD START (obj=50000437, id = 200008, name="SIGINT handler", group="system")
THREAD START (obj=50000513, id = 200009, name="Thread-0", group="main")
THREAD END (id = 200009)
THREAD END (id = 200008)
THREAD END (id = 200002)
TRACE 300509:
        java.net.PlainSocketImpl.socketAccept(PlainSocketImpl.java:Unknown line)
        java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
        java.net.ServerSocket.implAccept(ServerSocket.java:545)
        java.net.ServerSocket.accept(ServerSocket.java:513)
TRACE 300665:
        java.io.FileInputStream.readBytes(FileInputStream.java:Unknown line)
        java.io.FileInputStream.read(FileInputStream.java:255)
        sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
TRACE 300667:
        java.util.IdentityHashMap.keySet(IdentityHashMap.java:968)
        java.lang.ApplicationShutdownHooks.runHooks(ApplicationShutdownHooks.java:97)
        java.lang.ApplicationShutdownHooks$1.run(ApplicationShutdownHooks.java:46)
        java.lang.Shutdown.runHooks(Shutdown.java:123)
TRACE 300022:
        java.lang.Class.forName0(Class.java:Unknown line)
        java.lang.Class.forName(Class.java:348)
        sun.nio.cs.FastCharsetProvider.lookup(FastCharsetProvider.java:118)
        sun.nio.cs.FastCharsetProvider.charsetForName(FastCharsetProvider.java:133)
TRACE 300048:
        java.lang.Runtime.addShutdownHook(Runtime.java:211)
        java.util.logging.LogManager.<init>(LogManager.java:258)
        java.util.logging.LogManager.<init>(LogManager.java:251)
        java.util.logging.LogManager$1.run(LogManager.java:204)
TRACE 300084:
        java.lang.System.checkKey(System.java:840)
        java.lang.System.getProperty(System.java:714)
        com.sun.jmx.mbeanserver.GetPropertyAction.run(GetPropertyAction.java:44)
        com.sun.jmx.mbeanserver.GetPropertyAction.run(GetPropertyAction.java:36)
TRACE 300118:
        java.util.logging.LogManager$LoggerContext.getOwner(LogManager.java:646)
        java.util.logging.LogManager$LoggerContext.requiresDefaultLoggers(LogManager.java:639)
        java.util.logging.LogManager$LoggerContext.ensureDefaultLogger(LogManager.java:731)
        java.util.logging.LogManager$LoggerContext.ensureInitialized(LogManager.java:683)
TRACE 300192:
        java.lang.Class.isPrimitive(Class.java:Unknown line)
        javax.management.openmbean.ArrayType.getPrimitiveArrayType(ArrayType.java:869)
        com.sun.jmx.mbeanserver.DefaultMXBeanMappingFactory.<clinit>(DefaultMXBeanMappingFactory.java:223)
        com.sun.jmx.mbeanserver.MXBeanMappingFactory.<clinit>(MXBeanMappingFactory.java:102)
TRACE 300214:
        com.sun.jmx.mbeanserver.PerInterface$InitMaps.visitOperation(PerInterface.java:253)
        com.sun.jmx.mbeanserver.MBeanAnalyzer.visit(MBeanAnalyzer.java:74)
        com.sun.jmx.mbeanserver.PerInterface.<init>(PerInterface.java:54)
        com.sun.jmx.mbeanserver.MBeanIntrospector.getPerInterface(MBeanIntrospector.java:195)
TRACE 300253:
        java.lang.StringBuilder.append(StringBuilder.java:202)
        sun.reflect.generics.parser.SignatureParser.parseIdentifier(SignatureParser.java:268)
        sun.reflect.generics.parser.SignatureParser.parsePackageNameAndSimpleClassTypeSignature(SignatureParser.java:336)
        sun.reflect.generics.parser.SignatureParser.parseClassTypeSignature(SignatureParser.java:310)
TRACE 300268:
        sun.management.MemoryImpl.getMemoryPools0(MemoryImpl.java:Unknown line)
        sun.management.MemoryImpl.getMemoryPools(MemoryImpl.java:92)
        sun.management.ManagementFactoryHelper.getMemoryPoolMXBeans(ManagementFactoryHelper.java:112)
        java.lang.management.PlatformComponent$6.getMXBeans(PlatformComponent.java:144)
TRACE 300278:
        java.lang.String.startsWith(String.java:1434)
        com.sun.jmx.mbeanserver.MXBeanIntrospector.getMBeanAttributeInfo(MXBeanIntrospector.java:149)
        com.sun.jmx.mbeanserver.MXBeanIntrospector.getMBeanAttributeInfo(MXBeanIntrospector.java:54)
        com.sun.jmx.mbeanserver.MBeanIntrospector$MBeanInfoMaker.visitAttribute(MBeanIntrospector.java:324)
TRACE 300282:
        java.util.TreeMap$EntrySet.<init>(TreeMap.java:1061)
        java.util.TreeMap.entrySet(TreeMap.java:889)
        javax.management.ImmutableDescriptor.<init>(ImmutableDescriptor.java:107)
        javax.management.ImmutableDescriptor.<init>(ImmutableDescriptor.java:74)
TRACE 300302:
        java.lang.String.<init>(String.java:207)
        java.lang.StringBuilder.toString(StringBuilder.java:407)
        sun.management.DiagnosticCommandImpl.transform(DiagnosticCommandImpl.java:256)
        sun.management.DiagnosticCommandImpl.getMBeanInfo(DiagnosticCommandImpl.java:172)
TRACE 300310:
        java.util.Hashtable.get(Hashtable.java:362)
        java.util.logging.LogManager$LoggerContext.findLogger(LogManager.java:693)
        java.util.logging.LogManager.getLogger(LogManager.java:1226)
        java.util.logging.LogManager.demandLogger(LogManager.java:551)
TRACE 300343:
        java.io.BufferedReader.readLine(BufferedReader.java:313)
        java.io.BufferedReader.readLine(BufferedReader.java:389)
        sun.misc.MetaIndex.registerDirectory(MetaIndex.java:183)
        sun.misc.Launcher$BootClassPathHolder$1.run(Launcher.java:401)
TRACE 300392:
        java.lang.Double.toString(Double.java:204)
        java.lang.String.valueOf(String.java:3141)
        java.security.Provider.putId(Provider.java:688)
        java.security.Provider.<init>(Provider.java:138)
TRACE 300417:
        sun.security.provider.SecureRandom.init(SecureRandom.java:102)
        sun.security.provider.SecureRandom.<init>(SecureRandom.java:79)
        sun.security.provider.NativePRNG$RandomIO.getMixRandom(NativePRNG.java:387)
        sun.security.provider.NativePRNG$RandomIO.implNextBytes(NativePRNG.java:483)
TRACE 300464:
        sun.rmi.transport.DGCImpl$2.run(DGCImpl.java:295)
        sun.rmi.transport.DGCImpl$2.run(DGCImpl.java:281)
        java.security.AccessController.doPrivileged(AccessController.java:Unknown line)
        sun.rmi.transport.DGCImpl.<clinit>(DGCImpl.java:281)
TRACE 300508:
        sun.misc.PerfCounter$CoreCounters.<clinit>(PerfCounter.java:131)
        sun.misc.PerfCounter.getParentDelegationTime(PerfCounter.java:167)
        java.lang.ClassLoader.loadClass(ClassLoader.java:427)
        java.lang.ClassLoader.loadClass(ClassLoader.java:357)
TRACE 300548:
        sun.reflect.ByteVectorImpl.add(ByteVectorImpl.java:62)
        sun.reflect.ClassFileAssembler.emitByte(ClassFileAssembler.java:74)
        sun.reflect.ClassFileAssembler.emitConstantPoolNameAndType(ClassFileAssembler.java:119)
        sun.reflect.AccessorGenerator.emitBoxingContantPoolEntries(AccessorGenerator.java:359)
TRACE 300605:
        java.util.concurrent.ConcurrentHashMap.tabAt(ConcurrentHashMap.java:755)
        java.util.concurrent.ConcurrentHashMap.transfer(ConcurrentHashMap.java:2421)
        java.util.concurrent.ConcurrentHashMap.addCount(ConcurrentHashMap.java:2288)
        java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1070)
TRACE 300638:
        sun.management.ConnectorAddressLink.exportRemote(ConnectorAddressLink.java:140)
        sun.management.jmxremote.ConnectorBootstrap.startRemoteConnectorServer(ConnectorBootstrap.java:479)
        sun.management.Agent.startAgent(Agent.java:257)
        sun.management.Agent.startAgent(Agent.java:447)
TRACE 300669:
        java.util.logging.LogManager$LoggerContext.requiresDefaultLoggers(LogManager.java:637)
        java.util.logging.LogManager$LoggerContext.ensureDefaultLogger(LogManager.java:731)
        java.util.logging.LogManager$LoggerContext.ensureInitialized(LogManager.java:684)
        java.util.logging.LogManager$LoggerContext.findLogger(LogManager.java:692)
CPU SAMPLES BEGIN (total = 814) Sun Oct 22 15:35:36 2017
rank   self  accum   count trace method
   1 72.97% 72.97%     594 300509 java.net.PlainSocketImpl.socketAccept
   2 24.08% 97.05%     196 300665 java.io.FileInputStream.readBytes
   3  0.37% 97.42%       3 300667 java.util.IdentityHashMap.keySet
   4  0.12% 97.54%       1 300022 java.lang.Class.forName0
   5  0.12% 97.67%       1 300048 java.lang.Runtime.addShutdownHook
   6  0.12% 97.79%       1 300084 java.lang.System.checkKey
   7  0.12% 97.91%       1 300118 java.util.logging.LogManager$LoggerContext.getOwner
   8  0.12% 98.03%       1 300192 java.lang.Class.isPrimitive
   9  0.12% 98.16%       1 300214 com.sun.jmx.mbeanserver.PerInterface$InitMaps.visitOperation
  10  0.12% 98.28%       1 300253 java.lang.StringBuilder.append
  11  0.12% 98.40%       1 300268 sun.management.MemoryImpl.getMemoryPools0
  12  0.12% 98.53%       1 300278 java.lang.String.startsWith
  13  0.12% 98.65%       1 300282 java.util.TreeMap$EntrySet.<init>
  14  0.12% 98.77%       1 300302 java.lang.String.<init>
  15  0.12% 98.89%       1 300310 java.util.Hashtable.get
  16  0.12% 99.02%       1 300343 java.io.BufferedReader.readLine
  17  0.12% 99.14%       1 300392 java.lang.Double.toString
  18  0.12% 99.26%       1 300417 sun.security.provider.SecureRandom.init
  19  0.12% 99.39%       1 300464 sun.rmi.transport.DGCImpl$2.run
  20  0.12% 99.51%       1 300508 sun.misc.PerfCounter$CoreCounters.<clinit>
  21  0.12% 99.63%       1 300548 sun.reflect.ByteVectorImpl.add
  22  0.12% 99.75%       1 300605 java.util.concurrent.ConcurrentHashMap.tabAt
  23  0.12% 99.88%       1 300638 sun.management.ConnectorAddressLink.exportRemote
  24  0.12% 100.00%       1 300669 java.util.logging.LogManager$LoggerContext.requiresDefaultLoggers
CPU SAMPLES END
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容