2021-01-17 JVM-性能调优

垃圾收集器分类

垃圾收集器分类.png
  • 串行收集器->Serial和Serial Old

    只能有一个垃圾回收线程执行,用户线程暂停适用于'内存比较小的嵌入式设备。
    
  • 并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old

    多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 适用于'科学计算、后台处理等若交互场景 。
    
  • 并发收集器[停顿时间优先]->CMS、G1

    用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的
    
    时候不会停顿用户线程的运行。 适用于'相对时间有要求的场景,比如Web 。
    
  • 优先调整堆的大小让服务器自己来选择 如果内存小于100M,使用串行收集器 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选 如果允许停顿时间超过1秒,选择并行或JVM自己选 如果响应时间最重要,并且不能超过1秒,使用并发收集器

常见问题

1. 吞吐量和停顿时间

吞吐量和停顿时间

  • 停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间

  • 吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)

    停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 
    高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
    

    小结 :这两个指标也是评价垃圾回收器好处的标准。

2. 如何选择合适的垃圾收集器

官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28

  • 优先调整堆的大小让服务器自己来选择

  • 如果内存小于100M,使用串行收集器

  • 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选

  • 如果允许停顿时间超过1秒,选择并行或JVM自己选

  • 如果响应时间最重要,并且不能超过1秒,使用并发收集器

    case:G1 GC 默认最小内存是 2G,稳定运行内存是6G

    如果服务器是4C8G 且对响应时间要求不是特别高,可用使用G1;

    如果是CPU特别好且内存较小(PS:核数和内存占比较高,如8C4G),对响应时间要求最高,使用CMS。

  1. 内存泄漏与内存溢出的区别

    内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
    内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。

  2. young gc会有stw吗?

    不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关 系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS 和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时 间相对来说就小很多了。

  3. major gc和full gc的区别

    Major GC在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC 和 Full GC。一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触 发 Full GC 的原因有很多:当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大 时,会触发 Full GC;当老年代的空间使用率超过某阈值时,会触发 Full GC;当元空间不足时(JDK1.7 永久代不足),也会触发 Full GC;当调用 System.gc() 也会安排一次 Full GC。

  4. 什么是直接内存

    Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通 常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内 存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是 有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

  5. 垃圾判断的方式

    引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没 有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况。
    引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有 一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收。

    GC ROOT:

    • Class 由System Class Loader/Boot Class Loader加载的类对象,这些对象不会被回收。需 要注意的是其它的Class Loader实例加载的类对象不一定是GC root,除非这个类对象恰好 是其它形式的GC root;

    • Thread 线程,激活状态的线程;

    • Stack Local 栈中的对象。每个线程都会分配一个栈,栈中的局部变量或者参数都是GC root,因为它们的引用随时可能被用到;

    • JNI Local JNI中的局部变量和参数引用的对象;可能在JNI中定义的,也可能在虚拟机中定 义

    • JNI Global JNI中的全局变量引用的对象;同上

    • Monitor Used 用于保证同步的对象,例如wait(),notify()中使用的对象、锁等。

    • Held by JVM JVM持有的对象。JVM为了特殊用途保留的对象,它与JVM的具体实现有关。比如有System Class Loader, 一些Exceptions对象,和一些其它的Class Loader。对于这些类,JVM也没有过多的信息。

  6. 不可达的对象一定要被回收吗?

    即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真 正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行 一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个 对象建立关联,否则就会被真的回收。

  7. 为什么要区分新生代和老年代?

    当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不 同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合 适的垃圾收集算法。 比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制 成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分 配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

  8. G1与CMS的区别是什么

    CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 Mix GC;G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的 产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。

  9. 方法区中的无用类回收

    方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。 类需要同时满足下面 3 个条件才能算是 “无用的类” :
    a. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    b. 加载该类的 ClassLoader 已经被回收。
    c. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

JVM性能优化指南

JVM性能优化.png

JVM的性能优化可以分为代码层面非代码层面。 在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码,提取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。 在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。

注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身 已经做了很多的内部优化操作,大家要注意的 是不要为了调优和调优

JVM编译

JVM采取的是混合模式,也就是解释+编译的方式,对于大部分不常用的代码,不需要浪费时间将其编译成机器码,只需要用到的时候再以解释的方式运行;对于小部分的热点代码,可以采取编译的方式, 追求更高的运行效率。

解释器

Interpreter,解释器逐条把字节码翻译成机器码并执行,跨平台的保证。

刚开始执行引擎只采用了解释执行的,但是后来发现某些方法或者代码块被调用执行的特别频繁时,就 会把这些代码认定为"热点代码"。

即使编译器(JIT)-- JDK7各层次的优化。

Just-In-Time compilation(JIT),即时编译器先将字节码编译成对应平台的可执行文件,运行速度快。

即时编译器会把这些"热点代码"编译成与本地平台关联的机器码,并且进行各层次的优化,保存到内存
中。

执行引擎

热点代码:被多次调用的方法、被多次执行的循环体。说明:如果是循环体的话,会对其所在的方法进行即时编译 -- OSR.

如何判断是否为热点代码? -- 热点探测

热点探测分为两种:

  • 基于采样分析-- 无法解决线程阻塞造成方法一直在栈中的问题

  • 基于计数器的热点探测

    1. 方法调用计数器 : C1模式下 默认1500次;C2模式是10000次 可通过参数:-XX:CompileThreadHold来设置

      解释:统计的是一定时间内方法的调用次数,如果一定时间内调用次数小于设置的次数如1500,那么下个周期,调用次数设置为700次 -- 此算法为热度衰减算法 可通过参数:-XX:CounterHalfLifeTime来设置

    2. 回边计数器::统计的是循环体调用的次数,此处和方法调用计数器的区别是:统计的是绝对次数

热点探测方法计数器执行流程.png

热点探测回边计数器执行流程.png

JVM编译期自我优化--方法内联:如果static、final 修饰的方法a调用了方法b且方法b不是太大的情况下JVM在编译阶段会将方法b的内容直接合并进方法a中,方法b大小限制热点方法:325字节;,非热点方法35字节。

JVM编译期自我优化--逃逸分析:确定我们的对象会不会被外部访问到,如果一个对象仅仅是在方法中,那么可以直接将对象分配到栈(栈帧是线程私有的)中,分析过程就是逃逸分析,逃逸分析的目的是:分析对象的作用域,减少对象创建、回收的时间,同步锁的锁消除

逃逸分析命令:-XX:+DoEscapeAnalysis , 系统默认开启。

C1 -- C1也称为Client Compiler,适用于执行时间短或者对启动性能有要求的程序; 适用于:带界面的方法调用。

C2 -- C2也称为Server Compiler,适用于执行时间长或者对峰值性能有要求的程序;适用于:服务层面的方法调用。

JIT5各级别.png
方法JIT流程分类.png

开启分层编译的命令:

  • -XX:+TieredCompilation 默认开启,关闭直接走C2;

  • -XX:TieredStopAtLevel = 1表示只走C1;

内存

内存分配

正常情况下不需要设置,那如果是促销或者秒杀的场景呢? 每台机器配置2c4G,以每秒3000笔订单为例,整个过程持续60秒

秒杀场景JVM调优之内存分配.png
内存溢出(OOM)

一般会有两个原因: (1)大并发情况下 (2)内存泄露导致内存溢出

大并发[秒杀]

浏览器缓存、本地缓存、验证码

CDN静态资源服务器
集群+负载均衡 动静态资源分离、限流[基于令牌桶、漏桶算法] 应用级别缓存、接口防刷限流、队列、Tomcat性能优化 异步消息中间件
Redis热点数据对象缓存
分布式锁、数据库锁 5分钟之内没有支付,取消订单、恢复库存等

内存泄露导致内存溢出

ThreadLocal引起的内存泄露,最终导致内存溢出

public class TLController {
 @RequestMapping(value = "/tl")
 public String tl(HttpServletRequest request) {
     ThreadLocal<Byte[]> tl = new ThreadLocal<Byte[]>();
     // 1MB
     tl.set(new Byte[1024*1024]);
     return "ok";
} }

排查流程:

  1. 启动

  2. 使用jmeter模拟10000次并发

  3. top命令查看

    top
    top -Hp PID

  4. jstack查看线程情况,发现没有死锁或者IO阻塞的情况

    jstack PID
    java -jar arthas.jar ---> thread

  5. 查看堆内存的使用,发现堆内存的使用率已经高达88.95%

    jmap -heap PID
    java -jar arthas.jar ---> dashboard

  6. 此时可以大体判断出来,发生了内存泄露从而导致的内存溢出,那怎么排查呢?

    jmap -histo:live PID | more

    获取到jvm.hprof文件,上传到指定的工具分析,比如heaphero.io

GC

此处以G1垃圾收集器调优为例

是否选用G1

官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases

  1. 50%以上的堆被存活对象占用
  2. 对象分配和晋升的速度变化非常大
  3. 垃圾回收时间比较长
G1调优
  1. 使用G1GC垃圾收集器: -XX:+UseG1GC

    修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

    Throughput       Min Pause       Max Pause      Avg Pause       GC count
      99.16%         0.00016s         0.0137s        0.00559s          12
    
  2. 调整内存大小再获取gc日志分析

    -XX:MetaspaceSize=100M
    -Xms300M
    -Xmx300M
    

    比如设置堆内存的大小,获取到gc日志,使用GCViewer分析吞吐量和响应时间

    Throughput       Min Pause       Max Pause      Avg Pause       GC count
      98.89%          0.00021s        0.01531s       0.00538s           12
    
  3. 调整最大停顿时间

    -XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标
    

    比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间

    Throughput       Min Pause       Max Pause      Avg Pause       GC count
      98.96%          0.00015s        0.01737s       0.00574s          12
    
  1. 启动并发GC时堆内存占用百分比

    -XX:InitiatingHeapOccupancyPercent=45 G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行 GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
    

    比如设置该百分比参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

    Throughput       Min Pause       Max Pause      Avg Pause       GC count
      98.11%          0.00406s        0.00532s       0.00469s          12
    
G1调优最佳实战

官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations

  1. 不要手动设置新生代和老年代的大小,只要设置整个堆的大小
G1收集器在运行过程中,会自己调整新生代和老年代的大小 其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标 如果手动设置了大小就意味着放弃了G1的自动调优
  参考:https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc
  1. 不断调优暂停时间目标
一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就 不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以 对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到 满足。
  1. 使用-XX:ConcGCThreads=n来增加标记线程的数量
IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过 低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。 IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高 ConcGCThreads。
  1. MixedGC调优
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent
-XX:G1MixedGCCountTarger
-XX:G1OldCSetRegionThresholdPercent
  1. 适当增加堆内存大小

  2. 不正常的Full GC

有时候会发现系统刚刚启动的时候,就会发生一次Full GC,但是老年代空间比较充足,一般是由 Metaspace区域引起的。可以通过MetaspaceSize适当增加其大家,比如256M。

CPU占用率高

排查流程:

  1. top
  2. top -Hp PID 查看进程中占用CPU高的线程id,即tid
  3. jstack PID | grep tid

JVM常用命令

jps --查看java进程

jps.png

jinfo

  1. 实时查看和调整JVM配置参数

  2. 查看用法

    jinfo -flag name PID 查看某个java进程的name属性的值

    jinfo -flag MaxHeapSize PID
    jinfo -flag UseG1GC PID
    
    jinfo查看java进行name值.png
  3. 修改

    注意:参数只有被标记为manageable的flags可以被实时修改

    jinfo -flag [+|-] PID
    jinfo -flag <name>=<value> PID
    
  4. 查看曾经赋过值的一些参数

    jinfo -flags PID
    
    jinfo查看历史值.png

jstat

  1. 查看虚拟机性能统计信息

  2. 查看类装载信息

    jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
    
jstat查看类装载信息.png
  1. 查看垃圾收集信息

    jstat -gc PID 1000 10
    

jstack

  1. 查看线程堆栈信息

  2. 用法

    jstack PID
    
    jstack用法.png
  3. 排查死锁案例

    //运行主类
    public class DeadLockDemo {
        public static void main(String[] args) {
            DeadLock d1 = new DeadLock(true);
            DeadLock d2 = new DeadLock(false);
            Thread t1 = new Thread(d1);
            Thread t2 = new Thread(d2);
            t1.start();
            t2.start();
        }
    
    }
        //定义锁对象 class MyLock{
        public static Object obj1 = new Object();
        public static Object obj2 = new Object();
    }
    
    //死锁代码
    class DeadLock implements Runnable {
        private boolean flag;
    
        DeadLock(boolean flag) {
            this.flag = flag;
        }
    
        public void run() {
            if (flag) {
                while (true) {
                    synchronized (MyLock.obj1) {
                        System.out.println(Thread.currentThread().getName() + "---获得obj1锁");
    
                        synchronized (MyLock.obj2) {
                            System.out.println(Thread.currentThread().getName() + "---获得obj1锁");
                        }
                    }
                }
            } else {
                while (true) {
                    synchronized (MyLock.obj2) {
                        System.out.println(Thread.currentThread().getName() + "---获得obj2锁");
    
                        synchronized (MyLock.obj1) {
                            System.out.println(Thread.currentThread().getName() + "---获得obj1锁");
                        }
                    }
                }
            }
        }
    }
    
    • 运行结果:
    案例死锁-运行结果.png
    • jstack分析

      jstack-死锁分析1.png

      把打印信息拉到最后可以发现

      jstack-死锁分析(2)

      (2).png)

jmap

  1. 打印出堆内存相关信息

  2. dump出堆内存相关信息

    jmap -heap PID
    jinfo -flag UsePSAdaptiveSurvivorSizePolicy 35352
    -XX:SurvivorRatio=8
    
    jmap-打印堆栈信息
  3. 内存溢出自动dump出该文件。

    jmap -dump:format=b,file=heap.hprof PID
    
    jmapdump日志信息.png
  4. 要是在发生堆内存溢出的时候,能自动dump出该文件就好了

    一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件 
    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
    
  5. 关于dump下来的文件

    一般dump下来的文件可以结合工具来分析,如内存分析工具--MAT

JVM工具

  • 工具层面,会有非常多的场景,这时候首选不要嵌入的工具 -- 影响CPU算例,占用服务器资源
    提醒:Tprofiler可以使用一下,原因是:1. 可以随时开关,2. 可以精确分析创建了多少对象、哪行代码出现了问题。

内存分析工具--MAT

Java堆分析器,用于查找内存泄漏

Heap Dump,称为堆转储文件,是Java进程在某个时间内的快照

它在触发快照的时候保存了很多信息:Java对象和类信息。

通常在写Heap Dump文件前会触发一次Full GC。

下载地址 :https://www.eclipse.org/mat/downloads.php

  1. 获取dump文件

    • 手动

      jmap -dump:format=b,file=heap.hprof 44808
      
    • 自动

      -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
      
  1. Dump的信息

    • All Objects
      Class, fields, primitive values and references
    • All Classes
      Classloader, name, super class, static fields
    • Garbage Collection Roots
      Objects defined to be reachable by the JVM
    • Thread Stacks and Local Variables
      The call-stacks of threads at the moment of the snapshot, and per-frame information about local
      objects
  2. 使用

    • Histogram:可以列出内存中的对象,对象的个数及其大小

      Class Name:类名称,java类名 Objects:类的对象的数量,这个对象被创建了多少个
      Shallow Heap:一个对象内存的消耗大小,不包含对其他对象的引用
      Retained Heap:是shallow Heap的总和,即该对象被GC之后所能回收到内存的总和

      右击类名--->List Objects--->with incoming references--->列出该类的实例

      右击Java对象名--->Merge Shortest Paths to GC Roots--->exclude all ...--->找到GC Root以及原因

    • Leak Suspects:查找并分析内存泄漏的可能原因

      Reports--->Leak Suspects--->Details

    • Top Consumers:列出大对象

性能分析工具--Tprofiler

官网:https://github.com/alibaba/TProfiler/wiki

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

推荐阅读更多精彩内容