抽丝剥茧之JVM-线程堆栈

分享是一种精神,是加深理解最好的方式,没有之一!

前言

本次分享以及后续章节旨在提升同学们对JVM的认知度,hold住她!干!
1、解决一般线上JVM故障(cpu,内存,gc等问题)
2、了解JVM基本结构和原理,以及常用的性能优化手段
3、掌握常用的JVM-分析工具
4、十分钟定位问题

开车了!

常用工具

线程堆栈

线程堆栈也称线程调用堆栈,是虚拟机中线程(包括锁)状态的一个瞬间快照,即系统在某一个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况。
线程堆栈一般包含以下关键信息

  • 线程名字,id,线程的数量等
  • 线程的运行状态,锁的状态
  • 调用堆栈 即是所执行的方法,落地到具体的代码行数

so!借助堆栈信息可以帮助分析很多问题,如线程死锁,锁竞争,死循环,large线程耗时操作等等。在多数情况下,无须介入代码亦可对系统进行分析和故障定位!
线程堆栈一般可解决CPU相关的如下问题

  • 系统无缘无故的cpu过高(区分us和sy)
  • 系统挂起,无响应
  • 性能瓶颈(如无法充分利用cpu等)
  • 线程死锁,死循环等
  • 线程数量太多导致的内存溢出

vmstat pidstat 亦可分析CPU消耗情况,结合线程堆栈来分析更佳,但是定位问题还是优先分析dump文件,本文先忽略......

如何分析进程or线程堆栈?
  • 常龟操作
获取进程id
jps 
获取进程堆栈
jstack –l <pid> >> thread.dump 
根据进程pid获取线程tid详情
top -H -p <pid>
根据线程tid 搜索进程堆栈
cat thread.dump  | grep tid...
查询进程对应的线程数 
cat /proc/进程id/status | grep Threads
  • jps -v ( 传递给JVM的完整参数)
10754 identify-webapi-0.0.1.war -verbose:gc -Xloggc:/data/program/cn.xxx/xxx-api/0.0.1/gc.log -
XX:+PrintGCDetails -XX:+PrintGCDateStamps -
XX:+HeapDumpOnOutOfMemoryError -
XX:HeapDumpPath=/data/program/cn.xxx/xxx-webapi/0.0.1/java.hprof -
-Xms2g -Xmx2g 
-XX:MetaspaceSize=38m -XX:MaxMetaspaceSize=380m
XX:+UseConcMarkSweepGC -XX:CMSMaxAbortablePrecleanTime=5000 -XX:+CMSClassUnlo
28985 Jps -Dapplication.home=/xx/xxx/java8 -Xms8m

上述JVM参数 其实内容丰富
1、gc -Xloggc....配置了gc 日志的路径
2、XX:PrintGCDetails,就开启了详细GC日志模式,文件内容比较冗杂,亦可结合jvisualvm来分析。
3、XX:+HeapDumpOnOutOfMemoryError :JVM在遇到OOM(OutOfMemoryError)时生成Dump文件,同时可以指定文件路径
4、-Xms2g -Xmx2g 启动堆内存和最大堆内存一样
5、-XX:MetaspaceSize=38m -XX:MaxMetaspaceSize=380m 元空间初始值和最大值
6、UseConcMarkSweepGC:使用了cms垃圾收集器
........

可视化工具jvisualvm 初探...
$jps -l
9904 org.jetbrains.jps.cmdline.Launcher
9905 com.xxx.xxx.xxx.router.xxxApp
9878 org.jetbrains.idea.maven.server.RemoteMavenServer
......略.
VS
  • 线程堆栈可视化截图


上图重点关注的细节

  • 线程个数
  • 线程状态
  • 线程命名
  • 线程运行时间
    .....

ps:本次重点不在可视化工具,如果能理解JVM-堆栈内容,其实比可视化工具更强大,jvisualvm就是dump文件的的阉割版。

案例分析一 新手入门

线上CPU负载飙升到100%以上
top监控结果

$top
top - 11:37:34 up 100 days, 23:25,  1 user,  load average: 0.75, 0.70, 0.63 
Tasks:  32 total,   3 running,  29 sleeping,   0 stopped,   0 zombie
Cpu(s): 55.6%us,  3.0%sy,  0.0%ni, 41.3%id,  0.0%wa,  0.0%hi,  0.0%si,  0.1%st
   PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
18001 root      0 -20 5631m 4.0g 102m S 93.2 49.6   1024:13 java

其中CPU负载较高 达到了93% 最近十五分钟的负载都在60+%

  • tips: CPU 负载升高大部分表现为us用户线程或者sy系统线程占比太高
    本例us值高达 55.6% ,表示运行的应用程序消耗了大部分的 CPU,也就是说应用里面有所谓的大线程:large-thread
如何找到它?

根据常龟操作
1、jps定位到进程pid
2、根据进程id定位线程列表并根据cpu排序

$ top -H -p 10754


如图 表明线程tid=11979 cpu占比达99% 内存 60% ,
3、进制转换,11979是10进制数,在线程dump中线程id是十六进制,需要进一步转换

linux 终端 printf "%x\n" 11979 -> 2ecb -> 0x2ecb
OR
在线进制转换 十进制转换十六进制.

4、导出当前堆栈到文件中

jstack -l 10754 >> x.dump

5、搜索查询对应线程id运行明细

cat x.dump | grep 0x2ecb -A 30

结果如下

"http-nio-8848-exec-6" #31 daemon prio=5 os_prio=31 tid=0x00007ff87dae5800 nid=0x6b03 runnable [0x0000700010ef0000]
   java.lang.Thread.State: RUNNABLE
    at com.test.xxxx.identify.router.Looper.cpu_check(TestController.java:77)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)

  • 定位到具体代码-
    //模拟一段死循环
    Long i=0L;
    while(true){
      i++;
      if(i==-99){
        break;
      }
    }
  • 死循环类似电路系统中的"短路"
  • 上述案例是一种比较极端的情况,基友们可以拿这个来练手,寻找JVM中的"Big-Thread" 大线程
  • tips:线程如果出现了大循环,亦会飙高CPU
    可简单在循环体中加一句
for(....){
Thread.sleep(0);
or
Thread.sleep(1);
}

sleep(0)释放当前线程所剩余的时间片,让出CPU执行权,不要再"霸占"CPU",这样可以让操作系统切换其他线程来执行,也可以避免太多线程"饥饿" 提升效率

案例分析二

  • 线上入库上线后,CPU飙升到了100%, 用户线程us高达50%,可见有一批计算密集型的线程在run......
    万变不离其宗,还是案例一中的方法
  • 定位pid->定位tid->定位dump->定位代码(需要多次操作)
PID    USER      PR  NI  VIRT  RES  SHR S %CPU %MEM     
15707 root    20   0 6213m 2.5g 7844 S  1.3 65.5   
19811 root    20   0 6213m 2.5g 7844 S  1.0 65.5 
19992 root    20   0 6213m 2.5g 7844 S  1.0 65.5 
15693 root    20   0 6213m 2.5g 7844 S  1.0 65.5    
16362 root    20   0 6213m 2.5g 7844 S  1.0 65.5   
16443 root    20   0 6213m 2.5g 7844 S  1.0 65.5    
19812 root    20   0 6213m 2.5g 7844 S  0.7 65.5  
19814 root    20   0 6213m 2.5g 7844 S  0.7 65.5 
....略上百条....
  • 线程堆栈部分截取
"http-nio-8080-exec-261" #15738 daemon prio=5 os_prio=0 tid=0x00007fef60117800 nid=0x406b runnable [0x00007feefb50d000]
   java.lang.Thread.State: RUNNABLE
    at org.apache.ibatis.trace.TraceIntercept.replaceFirst(TraceIntercept.java:219)
    at org.apache.ibatis.trace.TraceIntercept.replacePlaceholder(TraceIntercept.java:196)
    at org.apache.ibatis.trace.TraceIntercept.getSql(TraceIntercept.java:172)
    at org.apache.ibatis.trace.TraceIntercept.intercept(TraceIntercept.java:98)
    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
    at com.sun.proxy.$Proxy162.update(Unknown Source)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update$impl(DefaultSqlSession.java:198)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:185)
    at sun.reflect.GeneratedMethodAccessor138.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
    at com.sun.proxy.$Proxy130.insert(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:278)
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:57)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at com.sun.proxy.$Proxy149.batchInsert(Unknown Source)
    at cn.xxx.xxx.xxx.dao.RawDataDAO.batchInsert(RawDataDAO.java:72)
    at cn.xxx.xxx.xxx.xxxx.batchInsert(RawDataManager.java:113)
    at cn.xxx.xxx.xxx.xxxx.batchRawData(RawdataStorageServiceImpl.java:101)
    at cn.xxx.xxx.controller.xxxxController.batchRawData(RawDataManagerController.java:60)
  • 定位到代码org.apache.ibatis.trace.TraceIntercept.replaceFirst
    mysql中间件链路跟踪 sql 拦截器,记录sql执行耗时rt,用来发现"慢sql".
    查询一下这段sql链路跟踪代码出现的次数(多次操作)
$ grep -c 'org.apache.ibatis.trace.TraceIntercept.getSql' k1.dump k2.dump k3.dump
k1.dump:89
k2.dump:76
k3.dump:98
  • 纳尼,100不到的http用户线程 cpu 能飙升到100%+?
  • 拦截器sql拼接字符串处理正则+replace太剧烈
如何分析线程堆栈?
名词解释
"http." prio=1 tid=0x000000000238e800 nid=0xc940 runnable [0x00000000027af000].
    |       |   |                       |           |           |
    |       |   |                       |           |           +--线程占用内存地址
    |       |   |                       |           +-----------线程的状态
    |       |   |                       +----线程对应的本地线程id号
    |       |   +-------------------线程id
    |       +--------------------------线程优先级
    +-------------------------------线程名称
    

备注:

  • 线程命名很重要!
  • 大线程的话 线程状态 必定为RUNNABLE
  • 线程id和线程对应的本地线程id不是一个概念
    线程对应的本地线程id号,是指Java线程所对应的虚拟机中的本地线程。由于Java执行的实体是Java虚拟机,因此Java语言中的线程是依附于虚拟机中的本地线程来运行的,实际上是本地线程在执行Java线程代码。
  • 线程堆栈关键字 可以做很多事情
//查询tomcat-http线程数
grep -c 'http-nio-8080-exec' k2.dump
200
ps:200 是tomcat默认的最大线程数,多次堆栈查出http线程数都达到了200,此处说明max-threads有待增大...
//查询当前堆栈dubbo线程数
grep -c 'DubboServerHandler-10.3.10.110:20888' k3.dump
200
ps:200是dubbo默认的最大线程数,优化策略
dubbo线程命名方式:DubboServerHandler+ip+端口+...
//查询当前堆栈线程数
grep -c 'nid' k2.dump
537
//查询运行中的线程数
grep -c 'java.lang.Thread.State: RUNNABLE' k2.dump
228
//springboot tomcat-http线程数配置
server.tomcat.max-threads=400
//dubbo线程数配置
<dubbo:protocol name="dubbo" port="20788" threadpool="cached" threads="400"/>
锁的解读
  • 线程占有一个锁的时候,线程的堆栈中会打印--locked<锁地址>
"http-nio-8080-exec-38" #908 daemon prio=5 os_prio=0 tid=0x00007fa0fc040000 nid=0x440c runnable [0x00007fa0dc35d000]
   java.lang.Thread.State: RUNNABLE
    at java.xxx...
    ....略
    - locked <0x00000000937f16c0> (a java.io.BufferedInputStream)
  • 线程正在等待其它线程释放该锁--waiting to lock<锁地址>
    此时该段关键字waiting to lock会在线程堆栈中频繁出现
  • 线程处于BLOCKED状态,通过堆栈可以获取线程对应锁相关的线程
    同一把锁 locked和 waiting to lock 通常呈现一对多的情况..
    同理 线程状态 也是 RUNNABLE和BLOCKED也是一对多出现..
    此时应用处于激烈的锁竞争,(synchronized/lock...)cpu也有可能会飙升,当然更多的情况是系统某些请求"变得很慢".....
"processor-13" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f000]
java.lang.Thread.State: BLOCKED (on object monitor)
                at beans.ConnectionPool.getConnection(ConnectionPool.java:102)
                - waiting to lock <0xe0375410> (a beans.ConnectionPool)
                at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111)
                at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43)
  • 死锁 deadlock 堆栈内容一旦出现了 deadlock ,相关线程两个或多个会一直纠缠在一起,挂在那里一动也不动。
Found one Java-level deadlock:
=============================
Java stack information for the threads listed above:
===================================================
"com.xxx.xxx.jvm.deadlock.TestThread2":
    at com.xxx.xxx.jvm.deadlock.TestThread2.fun(TestThread2.java:35)
    - waiting to lock <0x00000000d77363d0> (a java.lang.Object)
    - locked <0x00000000d77363e0> (a java.lang.Object)
    at com.xxx.xxx.jvm.deadlock.TestThread2.run(TestThread2.java:22)
"com.xxx.xxx.jvm.deadlock.TestThread1":
    at com.xxx.xxx.jvm.deadlock.TestThread1.fun(TestThread1.java:33)
    - waiting to lock <0x00000000d77363e0> (a java.lang.Object)
    - locked <0x00000000d77363d0> (a java.lang.Object)
    at com.xxx.xxx.jvm.deadlock.TestThread1.run(TestThread1.java:20)

Found 1 deadlock.

ps :死锁线程如果不是很消耗cpu的线程,如高浮点计算,字符串处理,正则匹配、大循环等cpu密集型线程,cpu是不会飙升的!系统也不会崩溃。它只是挂在那里!同理 mysql、oracle出现死锁也不会影响其他sql执行。

线程状态

线程堆栈分析重点关注一下关键字出现频率
Runnable:太多-> 说明系统负载合理,吞吐量高
BLOCKED :线程阻塞,锁竞争激烈
WAITING/TIMED_WAITING: 表明大多数线程在睡觉,cpu利用率不高...

//查询堆栈中的线程状态分布列表
grep java.lang.Thread.State k2.dump | awk '{print $2$3$4$5}' | sort | uniq -c
      1 BLOCKED(onobjectmonitor)
    228 RUNNABLE
      5 TIMED_WAITING(onobjectmonitor)
     33 TIMED_WAITING(parking)
      9 TIMED_WAITING(sleeping)
      1 WAITING(onobjectmonitor)
    253 WAITING(parking)

案例分析三

接收到告警信息,一直持续....


登录到线上

查询进程15880对应的线程数
三次cat结果如下...
cat /proc/15880/status | grep Threads
Threads:    1809 /2310 /1980
$ top
top - 17:31:49 up 84 days,  2:59,  1 user,  load average: 0.59, 0.51, 0.49
Tasks: 160 total,   1 running, 158 sleeping,   1 stopped,   0 zombie
Cpu(s):  1.4%us,  34%sy, 39.1%wa
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
15880 appweb    20   0 7605m 2.5g 7136 S 57.8 64.6 854:33.19 java
  • 34%sy 系统线程达到了 34% 内核线程消耗占比略偏高!
  • 32.1%wa io消耗高,可能http抓包线程比较多(io先忽略)
SocketInputStream.socketRead0...屡见不鲜..
[0x0000xxxx1111] java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method) 

说明当前应用线程间上下文切换比较严重(不算特别严重,没到崩溃边缘) --> 主要原因之一 创建了太多线程.(猜测)

  • 线程堆栈分析
  • 此时创建了太多线程,绝大多数都是应用线程,在不了解代码的情况下最好的解决办法就是随机浏览,根据线程命名很容易发现同一类型的线程堆栈....
    不用搜索,直接查看线程dump
    快捷方法 可以先搜索一下关键字 RUNNABLE
    大量出现了一下RUNNABLE 线程的堆栈信息
"thread-129" #122 prio=5 os_prio=0 tid=0x00007fba682bc000 nid=0x3eee runnable [0x00007fb9ff622000]
   java.lang.Thread.State: RUNNABLE
    at okhttp3.RealCall.execute(RealCall.java:77)
    at cn.xxx.xxx.xxx.MyClient.send(MyClient.java:129)
    at cn.xxx.xxx.crawler.xxxCrawler.getDetail(FayuanCrawler.java:411)
    at cn.xxx.xxx.crawler.xxxxCrawler.parseSearchHtml(FayuanCrawler.java:236)
    at cn.xxx.xxx.crawler.xxxxCrawler.exec(FayuanCrawler.java:140)
    at cn.xxx.xxx.service.xx.lambda$handle$0(MessageHandle.java:41)
    at cn.xxx.xxx.service.MessageHandle$$Lambda$46/1501251393.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

堆栈关键字搜索次数

grep -c 'xxxxCrawler.exec' jstack.log
863
这还只是一部分....

最终定位到代码 xxxxCrawler.exec(.....
可能存在无节制的创建线程业务逻辑

   Executors.newCachedThreadPool();
   
   @RequestMapping("/start")
    public String startxxxxx() {
        new Thread(this::naxy).start();
        return "success";
    }


  • Alibaba code规范 -Java代码规范的老司机...







案例分析四

应用创建了合理的较多的线程,但是cpu使用率一直不高,吞吐量上不去!

  • 应用复盘
    计费kafka-消费端消息出现大量堆积,grail告警。
    增大线程数4->40个,查看cpu负载 和之前变化不大
    增加到100,cpu负载如故
    增加到250,cpu负载略增高。
    kafka消息任处于堆积中,任凭怎么折腾,cpu吞吐量上不去,干着急!

  • 导出线程堆栈dump,关注一下kafka消费线程,本应用kafka的线程命名为
    p-kafka-http-consumer 开头。
    先查看下kafka线程数

grep -c 'p-kafka-http-consumer' kop.log
100

随便打开一个kafka消费线程堆栈

"p-kafka-http-consumer-t5-for-wodlee.visitrecord" #81 daemon prio=5 os_prio=0 tid=0x00007f39e4bf6800 nid=0x1a37 waiting on condition [0x00007f3943ffe000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.xxx.xxx.xxx.consumer.Looper$1.perform(Looper.java:154)
    at com.xxx.xxx.kafka.consumer.Looper.performAction(Looper.java:106)
    at com.xxx.xxx.kafka.consumer.Looper.loop(Looper.java:78)
    at com.xxx.xxx.kafka.consumer.Looper.run(Looper.java:43)

发现大量的kafka消费线程 都处于 TIMED_WAITING (sleeping) 睡觉状态
查看处于RUNNABLE状态的 kafka线程,多次dump 发现
p-xxx-http-consumer处于运行中的kafka消费线程个数处于[0,1]之间
以上分析表明
kafka消费端,无论开启多少线程,进程中都只会有一个线程处于运行状态,其他剩余线程都在睡大觉。

  • 根据堆栈信息定位到kafka客户端代码Looper类154行左右
                switch (status) {
                case OK:
                    next = LooperState.PROCESSING;
                    break;
                case DEFER:
                case SERVER_ERROR:
                //表明出现了锁冲突,消费端rt 5ms左右,100个线程竞争,锁冲突的概率很高,
               //这里直接用sleep 来缓解,降低cpu负载
              //其实可以考虑采用CAS的方式fetch对应的offset消息
                case LOCK_CONFLICT:
                    next = LooperState.WAITING;
                    break;


        //.....略
       actions.put(LooperState.WAITING, new StateAction() {
            public LooperState perform() throws Exception {
                try {
                    int sleepTime = 1100;
                    if (consumer instanceof ParallelKafkaConsumer) {
                        ParallelKafkaConsumer pc = (ParallelKafkaConsumer) consumer;
                        int alive = pc.getLooperThreadNum() - pc.getDeadThreadNum();
                        if (alive > 1 && alive < 5) {
                            sleepTime = 1500;
                        } else if (alive >= 5) {
                            sleepTime = 2000;
                        }
                    }
                    Thread.sleep(sleepTime);
                } catch (InterruptedException ie) {
                }
                return LooperState.FETCHING;
            }
        });

kafka消费端fetch消息,如果发现当前状态为LOCK_CONFLICT,会执行一段1.5s-2s的sleep

  • 解决方案,增大了kafka-bridge-server拉取数据限制,一次拉取更多的消息,减少sleep的时间等等。

MORE

三克油

  • 希望本文能给各位道友提供一些帮助,答疑解惑。


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

推荐阅读更多精彩内容