50 jvm 性能优化

Jdk垃圾收集器迭代版本:
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
-XX:+PrintCommandLineFlags 参数可查看默认设置收集器类型
-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断
jinfo -flag UseParallelGC 37492 | jinfo -flag UseParallelOldGC 37492 |jinfo -flag UseG1GC 37492 相关垃圾回收器参数 进程ID
-XX:+PrintCommandLineFlags 查看命令行相关参数
java -XX:+PrintCommandLineFlags -version

image.png

Jdk7种核心的收集器
1.串行回收器:Serial、Serial old (采用单线程回收垃圾 适合于堆内存空间比较小 个人小项目

2.并行回收器:ParNew、Parallel Scavenge、Parallel old 多核多线程、堆内存空间比较大
3.并发回收器:CMS、G1(分区算法) 减少GC暂停用户线程时间尽可能最短。
并发垃圾收集和并行垃圾收集的区别
(A)、并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如ParNew、Parallel Scavenge、Parallel Old;

(B)、并发(Concurrent)

   指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
  用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;    
  如CMS、G1(也有并行);

Jdk7收集器组合的关系

image.png

新生代收集器:SerialGC、Parallel Scavenge、ParNew
老年代收集器:SerialOldGC、Parallel old、CMS
整体收集器:G1
JDK8(含Jdk8)之前的组合关系:
Serial/Serial 01d、Serial/CMS、 ParNew/Serial 01d、 ParNew/CMS、Paral1el Scavenge/Serial 01d、Paral1el Scavenge/Parallel 01d、G1;
GC日志评估指标

1.吞吐量 运行用户代码占总时间的比例
总运行时间:程序的运行时间(100s)+内存回收的时间 (1s)
比如程序运行时间100s/内存回收时间 垃圾回收1s 100/101=99%
2.GC负荷 与吞吐量相反,指应用花在GC上的时间百分比
总运行时间:程序的运行时间+GC内存回收的时间
1/101 1%

3.暂停时间 应用线程花在GC stop-the-world 的时间 暂时时间越小越好

4.GC频率 次数 GC频率越小,stw暂停时间越大 GC回收频率越小、stw越小的情况下GC回收频率越大
5.反应速度 从一个对象变成垃圾道这个对象被回收的时间

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
比如:应用程序运行了100s,其中垃圾收集花费1s,那么吞吐量占比为99%。
100/100+1

暂停时间:暂停时间”是指一个时间段内应用程序线程暂停总共消耗时间6s

串行垃圾回收器
SerialOldGC与SerialGC
最古老,最稳定、效率高、可能会产生较长的停顿
–新生代、老年代使用串行回收
–新生代复制算法
–老年代标记-压缩
-XX:+UseSerialGC
单线程在收集垃圾的时候,必须暂停当前所有的工作线程,直到清理垃圾完垃圾,工作线程才可以继续执行,Stop The World
-XX:+PrintCommandLineFlags -XX:+UseSerialGC
主要应用于:桌面应用程序堆内存空间很小

依然是HotSpot在Client模式下默认的新生代收集器;
也有优于其他收集器的地方:
简单高效(与其他收集器的单线程相比);
对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的

并行垃圾回收器
ParNew并行回收器
ParNew垃圾收集器是Serial收集器的多线程版本,主要用于新生代垃圾收集器
1、特点
除了多线程外,其余的行为、特点和Serial收集器一样;
如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等;
2.应用场景:
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
3.参数配置:
"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;
"-XX:+UseParNewGC":强制指定使用ParNew;
"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
-XX:+PrintCommandLineFlags -XX:+UseParNewGC

吞吐量Parallel Scavenge优先回收器
(A)、有一些特点与ParNew收集器相似
新生代收集器;
采用复制算法;
多线程收集;
(B)、主要特点是:它的关注点与其他收集器不同
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;
而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);
(A)、"-XX:MaxGCPauseMillis" (减少用户线程暂停时间)

  控制最大垃圾收集停顿时间,大于0的毫秒数;
  MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;
  因为可能导致垃圾收集发生得更频繁;

(B)、"-XX:GCTimeRatio" (吞吐量优先)
设置垃圾收集时间占总时间的比率,0<n<100的整数;
GCTimeRatio相当于设置吞吐量大小;

  垃圾收集执行时间占应用程序执行时间的比例的计算方法是:

  例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%--1/(1+19);

假设-XX:GCTimeRatio=99 ,则垃圾收集时间为1/(1+99),默认值为99,即1%时间用于垃圾收集。

MS垃圾收集器
CMS(Concurrent Mark Sweep)收集器,以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。
CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤:
1、初始标记(CMS initial mark)。
2、并发标记(CMS concurrent mark)。
3、重新标记(CMS remark)。
4、并发清除(CMS concurrent sweep)。
注意:“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
A.初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
B.并发标记阶段【也就说明不会阻碍业务线程继续执行,因为它所以还会有下面要说的“重新标记”阶段了】就是进行GC Roots Tracing【其实就是从GC Roots开始找到它能引用的所有其它对象】的过程。
C.重新标记阶段则是为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
D.CMS收集器的动作步骤如下图所示,在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的:

image.png

[图片上传中...(image.png-2db007-1599438215571-0)]

优缺点
优点:
1.CMS收集器初始化标记与重新标记只需要短暂的stw操作,不会非常耗时。
2.CMS收集器降低GC线程在清理垃圾过程中,对用户线程暂停的时间。
3.CMS收集器可以实现用户线程与GC线程同时执行,整体可以实现低延迟。
缺点:
1.CMS收集器不向其他的收集器是必须等到老年代堆内存满的时候,才开始清理堆内存垃圾,
而是提前根据设定的堆内存达到一定阈值的时候,开始清理堆内存垃圾,确保CMS在回收
垃圾的过程中有足够的空间支持。


image.png

2.当CMS在清理回收垃圾过程中,发现运行期间内存无法满足需要,就会开启
一次可能出现“Concurrent Mode Failure" 失败而导致另一次Full GC的产生,
虚拟机会临时采用备选方案:Serial Old收集器重新进行老年代垃圾收集,这时候停顿
时间非常长。
4.因为CMS采用标记清除算法,每次回收完垃圾会造成空间的不连续性,产生大量
的垃圾碎片问题,而只能使用空闲列表执行分配内存。

为什么CMS收集器采用标记清除而不是标记整理算法?
因为CMS收集器采用并行的方式,清除垃圾与用户线程可以同时运行,
为了保证用户线程与GC线程同时运行,所以采用标记清除算法,
如果采用标记整理算法,有可能会导致移动内存地址,会发生的stw问题。

优点总结:
1.并发收集器 GC线程可以与用户线程同时并发执行
2.降低用户线程等待时间
缺点总结:
1.会发生内存碎片化(标记清除)
2.用户线程空间不足,无法存放大对象的情况下,有可能会触发FULLGC
3.消耗CPU资源(与用户线程同时执行)
4.CMS收集器无法处理浮动垃圾 在并发标记阶段如果产生了新的垃圾对象

,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象
没有被及时的回收,从而只能在下一次执行GC时释放这些之前未被回收的对相关

CMS收集器参数设置
-XX:+UseConcMarkSweepGc 手动指定使用CMS收集器执行内存回收任务。
开启该参数后会自动将-XX: +UseParNewGc打开。即: ParNew (Young区用) +CMS (0ld区用) +Serial 0ld的组合。

-XX:CMS1nitiatingOccupanyFraction设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一 次CMS 回收。 JDK6 及以上版本默认值为92%
如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。

-XX: +UseCMSCompactAtFullCollection用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX:CMSFullGCsBeforeCompaction设置在执行多少次Full GC后对内存空间进行压缩整理。

-XX:ParallelCMSThreads 设置CMS的线程数量。
CMS 默认启动的线程数是(ParallelGCThreads+3)/4, ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMs收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。

新生代回收线程数:和当前cpu核数相等。
G1收集器实现原理
CMS收集器存在那些缺点
CMS收集器只适合于使用老年代,采用标记清除算法,可以实现GC线程与用户线程同时执行,减少STW时间。
缺点:
1.标记清除算法会产生大量的碎片化的问题,如果存放一个大对象的时候,有可能会频繁的引发FullGC 使用串行老年代收集器单线程清理堆内存垃圾。
所有工作线程都是会触发stw问题,导致工作线程全部阻塞等待
2.在以前收集器中,如果存放的对象大于的新生代内存空间,则直接晋升老年代,那么如果该对象不是很频繁使用,会非常浪费堆内存空间。
G1收集器可以使用到新生代和老年代。
为什么叫G1收集器
1.因为G1是一个并行/并发回收器,它把堆内存分割为很多不相关的区域(Region) (物理上 不连续的)。使用不同的Region来表示Eden、幸存者0(S0)区,幸存者(S1)1区,老年代等。
2.由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First) 。
3.G1 GC有计划地避免在整个Java 堆中进行全区域的垃圾收集。G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

什么是G1收集器
G1收集器是一款在server端运行的垃圾收集器,专门针对于拥有多核处理器和大内存的机器,在JDK 7u4版本发行时被正式推出,在JDK9中更被指定为官方GC收集器。它满足高吞吐量的同时满足GC停顿的时间尽可能短。G1收集器专门针对以下应用场景设计
G1收集器发展历程
1.2004发表:G1的第一篇paper(附录1)
2.2012年才在jdk1.7u4中可用
3.oracle官方计划在jdk9中将G1变成默认的垃圾收集器以替代CMS。
G1收集器的应用场景
G1 (Garbage一First) 是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征,JDK9中已经默认使用G1收集器,可以全功能的垃圾收集器,采用标记整理算法避免堆空间冗余性问题。
G1收集器优缺点

1.并行与并发
并行:G1收集器在回收时,可以实现多个GC线程同时执行 利用CPU多核利用率,但是会让用户线程暂停 触发stw机制。
并发:多个GC与用户线程用时执行,用户线程不会阻塞。
2.分代收集原理
G1收集器,也会分为新生代、老年代, 新生代eden、S0或者S1区域,但是不要求整个eden、S0或者S1区域具有连续性。
与之前的收集器不同,它可以收集新生代也可以收集老年代。
3.空间整合
之前我们所学习的CMS收集器采用标记清除算法,容易产生碎片化的问题且空间不连续性,而G1收集器划分成n多个不同的采用标记
压缩算法,没有产生碎片化的问题。分配大对象的时候,避免FullGC的问题,所以如果堆内存空间比较大,使用G1收集器更加有优势。
4.可预测的停顿时间模型 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
由于G1收集器采用分区模型,所以G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

缺点:
小内存的情况下使用cms收集器,大内存的情况下可以使用G1收集器。G1收集器6GB以上
G1收集器核心配置参数
JDK9已经默认开启了G1收集器,如果在JDK8开启G1收集器。需要配置
-XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB 到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。 也就是G1收集器最小堆内存应该是2GB以上,最大堆内存64GB
-XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标 ,默认值是200ms
-XX:ParallelGCThread 设置垃圾回收线程数 最大设置为8
-XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
-XX:+UseG1GC 设置开启G1收集器
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails -verbose:gc
G1日志内容分析

G1收集器分区原理

使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB, 2MB, 4MB, 8MB, 1 6MB, 32MB。
可以通过一 XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连续)的集合。通过Region的动态分配方式实现逻辑_上的连续。

image.png

一个region(分区)只能属于一个角色,有可能为eden区、S区、老年代等, E表示为Eden区、S区表示为S1,S0区,老年代O区 空白的表示为未使用的分配的内存。
H区存放巨型对象
为什么G1收集器需要设计巨型对象
在G1收集器中也有一个新的内存区域,称作为:Humongous (H)区(巨型对象),主要存放一些比较大的对象,一个对象大于region的一半时,称之为巨型对象,G1不会对巨型对象进行拷贝,回收时会考虑优先回收。

在以前收集器中,如果是一个大对象是直接放入到老年代中,而触发老年代GC不是很频繁,万一该大对象不是非常频繁的使用,则会非常
浪费我们堆内存,为了解决这个问题在G1收集器专门弄一个H区存放巨型对象,如果一个H区装不下的情况下,则会寻找连续H区存储,
如果还没有足够的空间,有可能会引发FULLGC.
G1收集器回收过程原理

G1收集器回收过程分为三个环节:
1.年轻代GC(Young GC )
2.新生代和并发标记过程( Concurrent Marking)
3.混合回收(Mixed GC )
young gc(新生代) 一> young gc + concurrent mark(新生代+并发标) 一> Mixed GC(混合回收)顺序,进行垃圾回收

G1收集器Rset问题(记忆集)

在一个region中可能会引入到其他的region,为了避免不需要的全局扫描,在每个region中都对应一个Remembered Set(记忆集),使用CarTable
记录每个region区相互引用的关系。


image.png

G1收集器写屏障

写屏障(Store Barrier),所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念):记录赋值的操作。
G1收集器CSet
收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。

候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。

由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的

新生代回收过程
当新生代eden区内存满的时候,G1年轻代收集器会采用并行多线程的方式清理堆内存垃圾,这时候会暂停所有用户的线程,让后新生代存活的对象会拷贝到S区或者老年代中,和我们在以前所学习的新生代收集器原理基本相同。

标记复制算法:
新生代GC+并发标记过程

在新生代进行回收时,进行GCRoot初始化标记与CMS实现原理基本相同,老年代达到堆内存空间阈值时,会实现并发标记(不会stw),jvm配置参数
-XX:InitiatingHeapOccupancyPercen=45%
在使用再次标记(修正的时候)的时候,G1中采用了比CMS更快的初始快照算法:snapshot一at一the一beginning (SATB)。三色标记算法
白:未被标记的对象
灰:对象被标记了,但是它的field还没有被标记。
黑:对象被标记了,且它的所有field也被标记完了
注意区别:
1.CMS中使用,漏标采用增量更新

2.G1中使用,漏标采用SATB
混合收集
当越来越多的对象晋升到老年代的时候,为了避免堆内存耗尽,会触发混合收集器,即Mixed GC。
回收整个Young Region(新生代区域),部分的老年代区域,如果G1无法有足够的空间复制对象的时候,有可能会引发FullGc

新生代垃圾收集
后台收集(并发收集)
混合式垃圾收集
FullGC收集

1.新生代垃圾收集
在新生代GC的时候会进行GCRoot的初始标记,老年代占用堆内存比例达到阈值
的时候,进行并发标记(并发标记不会stw)。

混合式收集
1.最终标记(会触发stw)
2.拷贝存活(会触发stw)

巨型对象:
一个对象大于region的一半时,称之为巨型对象,G1不会对巨型对象进行拷贝,
回收时会考虑优先回收。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350