pthread_create failed: couldn't allocate 1069056-bytes mapped space: Out of memory

启动线程OOM有两种情况

case 1:

pthread_create failed: couldn't allocate 1069056-bytes mapped space: Out of memory

pthread_create failed: couldn't allocate 1069056-bytes mapped space: Out of memory

Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Try again"

HeapInfo: checking storage, bytes_hprof_total: 0, bytes_available: 38325297152, bytes_fileMinNeeded: 814915040

HeapInfo: bytes_available: 38325297152, bytes_fileMinNeeded: 814915040

hprof: heap dump "/data/misc/hprof/heap-dump-1518384637-pid4012.hprof" starting...

case 2:

java.lang.OutOfMemoryError: Could not allocate JNI Env

java.lang.Thread.nativeCreate(Native Method)

java.lang.Thread.start(Thread.java:729)

针对这两种case.其中最为常见的是case1.不管是那种case分析的起点都是一致的.

1 java-->native

public synchronized void start() {

 nativeCreate(this, stackSize, daemon);

}

解释如下:

1.this:即Thread对象本身

2.stackSize:指定了新建Thread stack的大小,单位是字节.如果设置为0表示忽略.

            提高stackSize会减少StackOverFlow的发生,而降低stackSize会减少OOM的发生.

            该参数和平台相关,某些平台会忽略该参数.

3.daemon:是否为守护线程

2 native-->ART

路径:art/runtime/native/java_lang_Thread.cc

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size, jboolean daemon) {

    Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);

}

路径:art/runtime/thread.cc

       void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {

    Thread* child_thread = new Thread(is_daemon);

    //分配stack size

    stack_size = FixStackSize(stack_size);

    //java中每一个 java线程 对应一个 JniEnv 结构。这里的JniEnvExt 就是ART 中的 JniEnv

    //Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and do not have a good way to report this on the child's side.

    std::unique_ptr<JNIEnvExt> child_jni_env_ext(

    JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg));

    //创建线程的主要逻辑

    if (child_jni_env_ext.get() != nullptr) {

        pthread_create_result = pthread_create(&new_pthread,

                                               &attr,

                                               Thread::CreateCallback,

                                               child_thread);

    }


    //执行创建流程失败的收尾逻辑

    //分别对应异常case 2 & case 1

    std::string msg(child_jni_env_ext.get() == nullptr ?

    StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :

    StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));


    //抛出OOM异常

    soa.Self()->ThrowOutOfMemoryError(msg.c_str());

}

其中child_jni_env_ext.get() == nullptr对应case 2,  pthread_create对应case 1

-------

默认的stack szie

路径:art/runtime/thread.cc

static size_t FixStackSize(size_t stack_size) { 

    if (stack_size == 0) { 

    // GetDefaultStackSize是启动art时命令行的"-Xss="参数,Android 中没有该参数,因此为0.

    stack_size = Runtime::Current()->GetDefaultStackSize();

    }  

    // bionic pthread 默认栈大小是 1M

    stack_size += 1 * MB;

    if (Runtime::Current()->ExplicitStackOverflowChecks()) {    

        // 8K

        stack_size += GetStackOverflowReservedBytes(kRuntimeISA);

    } else {    

        // 1M + 8K + 8K

        stack_size += Thread::kStackOverflowImplicitCheckSize +

        GetStackOverflowReservedBytes(kRuntimeISA);

  }

  return stack_size;//1M + 8K + 8K = 1040K

}

因此默认的stack szie是1040KB.这个信息case 1中crash的信息一致

case 1.1

通过上面的分析主要逻辑是调用pthread_create.

pthread_create_result = pthread_create(&new_pthread,

                                               &attr,

                                               Thread::CreateCallback,

                                               child_thread);

参数解释:

new_pthread:新建线程句柄

attr:指定新建thread的属性,包括stack size

Thread::CreateCallback: 新创建的线程的routine函数,即,线程的入口函数。

child_thread: callbac的唯一参数,此处是 native 层的 Thread 类

case 1.2 ART-->pthread

路径:bionic/libc/bionic/pthread_create.cpp

int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr, void* (*start_routine)(void*), void* arg) { 

    //1.分配stack

    pthread_internal_t* thread = NULL; 

    void* child_stack = NULL; 

    int result = __allocate_thread(&thread_attr, &thread, &child_stack); 

    if (result != 0) { 

        return result;

    }

    //2.linux 系统调用 clone,执行真正的创建动作。

    int rc = clone(__pthread_start, child_stack, flags, thread, &(thread->tid), tls, &(thread->tid));

      if (rc == -1) {

          __libc_format_log(ANDROID_LOG_WARN, "libc", "pthread_create failed: clone failed: %s", strerror(errno));

}

}

static int __allocate_thread {

    mmap_size = BIONIC_ALIGN(attr->stack_size + sizeof(pthread_internal_t), PAGE_SIZE);

    attr->stack_base = __create_thread_mapped_space(mmap_size, attr->guard_size);

}

static void* __create_thread_mapped_space(size_t mmap_size, size_t stack_guard_size) {

    //调用mmap分配内存

    int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE;

    void* space = mmap(NULL, mmap_size, prot, flags, -1, 0); 

    //判断StackOverflow的场景,避免栈内存溢出污染其他内存区域

    //Stack is at the lower end of mapped space, stack guard region is at the lower end of stack.

    //Set the stack guard region to PROT_NONE, so we can detect thread stack overflow.

    if (mprotect(space, stack_guard_size, PROT_NONE) == -1) {

        munmap(space, mmap_size);

        return NULL;

    }

    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, space, stack_guard_size, "thread stack guard page"); 

   return space;

}

Case1.3 pthread-->Linux内核

int rc = clone(__pthread_start, child_stack, flags, thread, &(thread->tid), tls, &(thread->tid));

这里主要涉及到linux的clone系统调用(SystemCall)(http://man7.org/linux/man-pages/man2/clone.2.html)

man page中对clone的描述:

clone() creates a new process, in a manner similar to fork(2).

因为unix中只有进程的概念,所有clone是实现线程的一种手段.

Fork:创建新的进程.并将父进程的内存全部copy到子进程,也就说父子进程内存不共享.

Clone:创建新的进程,且父子进程共享内存

调用链

nativeCreate-->CreateNativeThread-->pthread_create-->__allocate_thread-->__create_thread_mapped_space-->mmap-->clone


Case 2.1 native-->ART

std::string msg(child_jni_env_ext.get() == nullptr ?

    StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :

    StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));

Case2发生的原因:JNIEnvExt::Create调用失败。

JNIEnvExt* JNIEnvExt::Create(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) {

  std::unique_ptr<JNIEnvExt> ret(new JNIEnvExt(self_in, vm_in, error_msg)); 

 if (CheckLocalsValid(ret.get())) { 

   return ret.release();

  }

     return nullptr;

}

直接原因是CheckLocalsValidreturn false,再进一步是JniEnvExt::table_mem_map_ 是nullptr.

调用链是 JniEnvExt::Create() -> JNIEnvExt::JNIEnvExt()(构造函数) -> IndirectReferenceTable::IndirectReferenceTable()    

const size_t table_bytes = max_count * sizeof(IrtEntry);

table_mem_map_.reset(MemMap::MapAnonymous(..., table_bytes, ...));

max_count是常量 art::kLocalsInitial == 512计算了一下sizeof(IrtEntry) == 8.所以 table_bytes = 512 * 8 = 4096 = 4k,刚好是一个内存页的大小.因此是调用MemMap::MapAnonymous() 失败了。

路径:art/runtime/mem_map.cc:

// 1. 创建 ashmemfd.reset(

  ashmem_create_region(debug_friendly_name.c_str(), page_aligned_byte_count));

// 2. 调用mmap映射到用户态内存地址空间void* actual = MapInternal(..., fd.get(), ...);

步骤1失败的话,fd.get()返回-1,步骤2仍然会正常执行,只不过其行为有所不同。

如果步骤1成功的话,两个步骤则是:

1.通过Andorid的匿名共享内存(Anonymous Shared Memory)分配 4KB(一个page)内核态内存

2.再通过 Linux 的 mmap 调用映射到用户态虚拟内存地址空间.

如果步骤1失败的话,步骤2则是:

    通过 Linux 的 mmap 调用创建一段虚拟内存.分配虚拟内存失败了

考察失败的场景:

    步骤1 失败的情况一般是内核分配内存失败,这种情况下,整个设备/OS的内存应该都处于非常紧张的状态。

    步骤2 失败的情况一般是 进程虚拟内存地址空间耗尽

OOM分析结论

不管是case 1,还是堆栈case 2:

    创建线程过程中发生OOM是因为进程内的虚拟内存地址空间耗尽了

Linux内存分布

什么情况下虚拟内存地址空间才会耗尽呢?32位系统中,用户空间的内存是3G大,简单起见,我们粗略估计一下,假设

1.可见虚拟内存是3G大(实际值更小)

2.创建一个进程需要1M虚拟内存(实际值更大)

因此再假设有一个进程,除了创建线程什么都不干,那他最多能创建多少个线程?3G/1M = 约3000个

在完全理想的情况下最多是3000个线程。综合其他因素,实际值会明显小于3000。虽然3000的上限看上去很大,而如果有代码逻辑问题,创建很多线程,其实很容易爆掉。

Case 1:抛出OOM的地方已经保留了错误码信息

pthread_create_result = pthread_create(...);

StringPrintf("pthread_create (%s stack) failed: %s",

             PrettySize(stack_size).c_str(), strerror(pthread_create_result)));

pthread_create failed: clone failed: Out of memory11-06 12:27:00.256 30775 31188 W art     : Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"

错误码定义

路径:bionic/lib/private/bionic_errdefs/bionic_errdefs.h

__BIONIC_ERRDEF( EBADF          ,   9, "Bad file descriptor" )

__BIONIC_ERRDEF( ECHILD         ,  10, "No child processes" )

__BIONIC_ERRDEF( EAGAIN         ,  11, "Try again" )

__BIONIC_ERRDEF( ENOMEM         ,  12, "Out of memory" )

__BIONIC_ERRDEF( EMFILE         ,  24, "Too many open files" )

Case 2:抛出OOM的地方已经保留了错误码信息,属于 FileDescriptor 耗尽

fd.Reset(ashmem_create_region(debug_friendly_name.c_str(), page_aligned_byte_count),  /* check_usage */ false);  

   if (fd.Fd() == -1) {

      *error_msg = StringPrintf("ashmem_create_region failed for '%s': %s", name, strerror(errno));

ashmem_create_region failed for 'indirect ref table': Too many open files11-06 06:25:54.193  3725  8575 W art     : Throwing OutOfMemoryError "Could not allocate JNI Env"

内存充足OOM

在堆内存,物理内存,SD卡空间充足时也会发生OOM

下面是几个关于 Android 官方声明内存限制阈值的 API:

ActivityManager.getMemoryClass()    虚拟机 java 堆大小的上限,分配对象时突破这个大小就会 OOM

ActivityManager.getLargeMemoryClass()manifest 中设置 largeheap=true 时虚拟机 java 堆的上限

Runtime.getRuntime().maxMemory()


当前虚拟机实例的内存使用上限,为上述两者之一

Runtime.getRuntime().totalMemory()当前已经申请的内存,包括已经使用的和还没有使用的

Runtime.getRuntime().freeMemory()                                    上一条中已经申请但是尚未使用的那部分。那么已经申请并且正在使用的部分 used=totalMemory() - freeMemory()

ActivityManager.MemoryInfo.totalMem:设备总内存

ActivityManager.MemoryInfo.availMem设备当前可用内存

/proc/meminfo记录设备的内存信息

堆内存不够用即Runtime.getRuntime().maxMemory() 这个指标满足不了申请堆内存大小时

Runtime.getRuntime().maxMemory() 这个指标满足不了申请堆内存大小时

此时通过 new byte[] 的方式尝试申请超过阈值 maxMemory() 的堆内存

通常这种 OOM 的错误信息通常如下

java.lang.OutOfMemoryError: Failed to allocate a XXX byte allocation with XXX free bytes and XXXKB until OOM

各个空间都充裕(堆内存Runtime.getRuntime().maxMemory() 大小的堆内存还剩余很大一部分,物理内存,SD卡ActivityManager.MemoryInfo.availMem 还有很多)但是依然抛出OOM即case1 & case2

Case 1:抛出异常信息如下

java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory

Case 2:抛出异常信息如下

java.lang.OutOfMemoryError: Could not allocate JNI Env

OOM抛出时机

JVM对throw OOM定义

路径:art/runtime/thread.cc

void Thread::ThrowOutOfMemoryError(const char* msg)

参数 msg 携带了 OOM 时的错误信息

重点关注如下几个抛出时机

1.堆操作,该种情况抛出其实是堆内存不够及申请的堆内存超过了Runtime.getRuntime().maxMemory()

路径:/art/runtime/gc/heap.cc

void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type)

抛出时的错误信息:

    oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free  << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";


2.创建线程

路径:/art/runtime/thread.cc

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon)

抛出时的错误信息:

    "Could not allocate JNI Env"

  或者

    StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));

系统限制

抛出 OOM,一定是线程创建过程中触发了某些限制,既然不是 Art 虚拟机为我们设置的堆上限,那么可能是更底层的限制。Android 系统基于 linux,所以 linux 的限制对于 Android 同样适用,这些限制有:

1 ./proc/pid/limits 描述着 linux 系统对对应进程的限制,下面是一个样例:

Limit                     Soft Limit           Hard Limit           Units     

Max cpu time              unlimited            unlimited            seconds   

Max file size             unlimited            unlimited            bytes     

Max data size             unlimited            unlimited            bytes     

Max stack size            8388608              unlimited            bytes     

Max core file size        0                    unlimited            bytes     

Max resident set          unlimited            unlimited            bytes     

Max processes             13419                13419                processes 

Max open files            1024                 4096                 files     

Max locked memory         67108864             67108864             bytes     

Max address space         unlimited            unlimited            bytes     

Max file locks            unlimited            unlimited            locks     

Max pending signals       13419                13419                signals   

Max msgqueue size         819200               819200               bytes     

Max nice priority         40                   40                   

Max realtime priority     0                    0                    

Max realtime timeout      unlimited            unlimited            us

Max stack size,Max processes 的限制是整个系统的,不是针对某个进程的

Max locked memory 线程创建过程中分配线程私有 stack 使用的 mmap 调用没有设置 MAP_LOCKED,所以这个限制与线程创建过程无关

Max pending signals,c 层信号个数阈值,这个限制与线程创建过程无关

Max msgqueue size,Android IPC 机制不支持消息队列

Max open files 最可疑,Max open files 表示每个进程最大打开文件的数目,进程 每打开一个文件就会产生一个文件描述符 fd(记录在 /proc/pid/fd 下面),这个限制表明 fd 的数目不能超过 Max open files 规定的数目

2 . /proc/sys/kernel 中描述的限制

这些限制中与线程相关的是 /proc/sys/kernel/threads-max,规定了每个进程创建线程数目的上限,所以线程创建导致 OOM 的原因也有可能与这个限制相关

Could not allocate JNI Env

触发大量网络连接且每个连接处于单独的线程中并保持/循环打开多个文件并保持/创建大量的handlerthread等等

当进程 fd 数(可以通过 ls /proc/pid/fd | wc -l 获得)突破 /proc/pid/limits 中规定的 Max open files 时,产生 OOM;

堆栈信息

ashmem_create_region failed for 'indirect ref table': Too many open files

java.lang.OutOfMemoryError: Could not allocate JNI Env

当 Thread.UncaughtExceptionHandler 捕获到 OutOfMemoryError 时记录 /proc/pid 目录下的如下信息:

1./proc/pid/fd 目录下文件数 (fd 数)

2./proc/pid/status 中 threads 项(当前线程数目)

3. OOM 的日志信息(出了堆栈信息还包含其他的一些 warning 信息

比较/proc/pid/fd 目录下文件数与 /proc/pid/limits 中的 Max open files 数目,验证FD数目

pthread_create

创建大量线程当线程数(可以在 /proc/pid/status 中的 threads 项实时查看)超过 /proc/sys/kernel/threads-max 中规定的上限时产生 OOM 崩溃.已用逻辑空间地址可以查看 /proc/pid/status 中VmPeak/VmSize 记录

堆栈信息

pthread_create failed: clone failed: Out of memory

Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"

验证/proc/pid/status 中 threads 记录是否到达上限

线程创建的简易版流程

线程创建大概有两个关键的步骤

1.创建线程私有的结构体 JNIENV(JNI 执行环境,用于 C 层调用 Java 层代码)

2.调用 posix C 库的函数 pthread_create 进行线程创建工作

说明:

1.节点①

路径:art/runtime/thread.cc

std::string msg(child_jni_env_ext.get() == nullptr ?

        "Could not allocate JNI Env" :

        StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));

    ScopedObjectAccess soa(env);

    soa.Self()->ThrowOutOfMemoryError(msg.c_str());

JNIENV 创建不成功时产生 OOM 的错误信息为 "Could not allocate JNI Env"

pthread_create失败时抛出 OOM 的错误信息为"pthread_create (%s stack) failed: %s".其中详细的错误信息由 pthread_create 的返回值(错误码)给出.

2. 节点②和③是创建JNIENV过程的关键节点

2.1节点②/art/runtime/mem_map.cc中函数MemMap:MapAnonymous 的作用是为 JNIENV 结构体中 Indirect_Reference_table(C 层用于存储 JNI 局部 / 全局变量)申请内存

if (fd.get() == -1) {

      *error_msg = StringPrintf("ashmem_create_region failed for '%s': %s", name, strerror(errno));

      return nullptr;}


2.2节点③申请内存的方法.函数ashmem_create_region(创建一块 ashmen 匿名共享内存, 并返回一个文件描述符)

3. 图中节点④和⑤是调用C库创建线程时的环节,创建线程首先调用 __allocate_thread 函数申请线程私有的栈内存(stack)等,然后调用 clone方法进行线程创建.申请 stack 采用的时 mmap 的方式,节点⑤代码节选如下:


if (space == MAP_FAILED) {

    __libc_format_log(ANDROID_LOG_WARN,

                      "libc",

                      "pthread_create failed: couldn't allocate %zu-bytes mapped space: %s",

                      mmap_size, strerror(errno));

    return NULL;

  }

节点④代码

导致OOM 发生的原因

1. 文件描述符 (fd) 数目超限,即 proc/pid/fd 下文件数目突破 /proc/pid/limits 中的限制。可能的发生场景有:短时间内大量请求导致 socket 的 fd 数激增,大量(重复)打开文件等 ;

2. 线程数超限,即proc/pid/status中记录的线程数(threads 项)突破 /proc/sys/kernel/threads-max 中规定的最大线程数。可能的发生场景有:app 内多线程使用不合理,如多个不共享线程池的 OKhttpclient 等等 ;

3. 传统的 java 堆内存超限,即申请堆内存大小超过了 Runtime.getRuntime().maxMemory();

4. (低概率)32 为系统进程逻辑空间被占满导致 OOM;

5. 其他

监控措施

可以利用 linux 的 inotify 机制进行监控:

watch /proc/pid/fd来监控 app 打开文件的情况,

watch /proc/pid/task来监控线程使用情况

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

推荐阅读更多精彩内容