JVM实战

1.Java代码是如何运行起来的?

1.Java代码是如何运行起来的.png
  1. 首先从".java"代码文件,编译成".class"字节码文件;
  2. 将".class"字节码文件通过java -jar等方式打成jar包或者war包的形式;
  3. 然后类加载器把".class"字节码文件中的类给加载到JVM中;
  4. 接着JVM执行我们写好的那些类中的代码。

2.类从加载到使用的几个阶段?

一个类从加载到到使用,一般会经历下面的过程:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

  1. 验证阶段:简单来说,就是根据Java虚拟机规范,来校验你加载进来的"class"字节码文件中的内容,是否符合指定的规范;
  2. 准备阶段:给加载进来的类分配一定的内容空间,然后给它里面的类变量(static修饰的变量)分配内存空间,并且赋予初始值;
  3. 解析阶段:把符号引用替换为直接引用;
  4. 初始化阶段:执行类的初始化代码;


    示例代码.png
示例代码的加载流程.png

3.什么时候会初始化一个类?

  1. 使用new 关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、一再编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候;
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候;
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化;

注意:所有引用类的方式都不会触发初始化,称为被动引用。
示例1:通过子类引用父类的静态字段,不会导致子类初始化

public class SuperClass {

    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}

public class SubClass extends SuperClass {

    static {
        System.out.println("SubClass init!");
    }
}

public class NotInitization {

    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

执行结果:


执行结果.png

示例2:通过数组定义来引用类,不会触发此类的初始化

public class NotInitization2 {

    public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}

执行结果:
发现并没有输出"SuperClass init!"。
示例3:常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

public class ConstClass {

    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORLD = "hello world";
}

public class NotInitization3 {

    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}

执行结果:
也没有输出"ConstClass init!"。这是因为虽然在Java源码中引用了ConstClass类中的常量HELLOWORLD,但其实在编译阶段通过常量传播优化,已经将此常量的值"hello world"存储到NotInitization3类的常量池中,以后NotInitization3对常量的引用实际上都被转化为NotInitization3类对自身常量池的引用了。

4.类加载器与双亲委派机制?

Java的4中类加载器:

  1. 启动类加载器
    BootStrap ClassLoader,他主要是负责加载我们在机器上安装的Java目录下的核心类的。在你的Java安装目录下,就有一个“lib”目录,这里就有Java最核心的一些类库,支撑你的Java系统的运行。所以一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库。
  2. 扩展类加载器
    Extension ClassLoader,这个类加载器其实也是类似的,就是你的Java安装目录下,有一个“lib\ext”目录,这里面有一些类,就是需要使用这个类加载器来加载的,支撑你的系统的运行。
    那么你的JVM一旦启动,是不是也得从Java安装目录下,加载这个“lib\ext”目录中的类。
  3. 应用程序类加载器
    Application ClassLoader,这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类,其实你大致就理解为去加载你写好的Java代码吧,这个类加载器就负责加载你写好的那些类到内存里。
  4. 自定义类加载器
    除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类。
    自己实现一个类加载器.

类加载的双亲委派机制


加载器的亲子层级结构.png

假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载,但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。


双亲委派机制加载示例.png

5.Java虚拟机里有哪些内存区域,分别都是用来干什么的?

演示代码:

public class Kafka {

    public static void main(String[] args) {
        ReplicaManager replicaManager = new ReplicaManager();
        replicaManager.loadReplicasFromDisk();
    }
}

public class ReplicaManager {

    private long replicaCount;

    public void loadReplicasFromDisk() {
        Boolean hasFinished = false;
        if (isLocalDataCorrupt()) {}
    }

    private boolean isLocalDataCorrupt() {
        Boolean isCorrupt = false;
        return isCorrupt;
    }
}

演示代码全流程示意图:


核心内存区域的全流程串讲.png

虚拟机栈与堆内存.png
  1. 首先,JVM启动,就会加载你的Kafka类到内存里。然后有一个main线程,开始执行你的kafka中的main()方法。
  2. main线程是关联了一个程序计数器的,那么它执行到那一条指令,就会记录在这里。
  3. 其次,main线程在执行main()方法的时候,会在main线程关联的Java虚拟机栈里,压入一个main()方法的栈帧。
  4. 接着会发现需要创建一个ReplicaManager类的实例对象,此时会加载ReplicaManager类到内存里来。
  5. 然后会创建一个ReplicaManager的对象实例分配在Java堆内存里,并且在main()方法的栈帧里的局部变量表引入一个“replicaManager”变量,让他引用ReplicaManager对象在Java堆内存中的地址。
  6. 接着,main线程开始执行ReplicaManager对象中的方法,会依次把自己执行到的方法对应的栈帧压入自己的Java虚拟机栈,执行完方法之后再把方法对应的栈帧从Java虚拟机栈里出栈。

7.tomcat的类加载机制,打破了双亲委派机制

tomcat类加载机制

8.方法区内的类是否会被垃圾回收?

在以下几种情况下,方法区里的类会被回收:

  1. 首先该类的所有实例对象都已经从Java堆内存里被回收;
  2. 其次加载这个类的ClassLoader已经被回收;
  3. 最后,对该类的Class对象没有任何引用。

9.对象在JVM内存中如何分配?如何流转?

示例代码:

public class Kafka {

    private static ReplicaFetcher fetcher = new ReplicaFetcher();

    public static void main(String[] args) throws InterruptedException {
        loadReplicasFromDisk();

        while (true) {
            fetchReplicasFromRemote();
            Thread.sleep(1000);
        }
    }

    private static void loadReplicasFromDisk() {
        ReplicaManager replicaManager = new ReplicaManager();
        replicaManager.load();
    }

    private static void fetchReplicasFromRemote() {
        fetcher.fetch();
    }
}

public class ReplicaFetcher {
    public void fetch() {
    }
}

public class ReplicaManager {
    
    public void load() {
    }
}
  1. 实例对象和静态对象都会先分配在新生代


    image.png
  2. 一旦“loadReplicasFromDisk()”方法执行完毕之后,这个方法的栈帧出栈,会导致没有任何局部变量引用那个“ReplicaManager”实例对象了


    image.png
  3. 我们会在新生代里分配大量的对象,但是使用完之后立马就没人引用了,此时新生代差不多满了然后要分配新的对象的时候,发现新生代内存空间不足,就会触发一次垃圾回收(young gc),然后就把所有垃圾对象给干掉,腾出大量的内存空间


    image.png
  4. 实例对象在新生代中,成功的在15次垃圾回收之后,还是没被回收掉,就说明他已经15岁了。这是对象的年龄,每垃圾回收一次,如果一个对象没被回收掉,他的年龄就会增加1。所以如果上图中的那个“ReplicaFetcher”对象在新生代中成功躲过10多次垃圾回收,成为一个“老年人”,那么就会被认为是会长期存活在内存里的对象。然后他会被转移到Java堆内存的老年代中去


    image.png

    流程总结:

  5. 对象优先分配在新生代;
  6. 新生代如果对象满了,会触发Minor GC回收掉没有人引用的垃圾对象;
  7. 如果有对象躲过了十多次垃圾回收,就会放入老年代里;
  8. 如果老年代也满了,那么也会触发垃圾回收,把老年代里没人引用的垃圾对象清理掉。

10.JVM内存相关的核心参数图解及如何设置JVM参数

-Xms:Java堆内存的大小
-Xmx:Java堆内存的最大大小
-Xmn:Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了
-XX:PermSize:永久代大小
-XX:MaxPermSize:永久代最大大小
-Xss:每个线程的栈内存大小

参数图解.png

如何在启动系统的时候设置JVM参数?

  1. 在IDEA中设置如何设置JVM参数


    image.png
  2. 启动一个jar包里的系统如何设置JVM参数
    java -Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M -jar App.jar
  3. Tomcat部署系统怎么设置JVM参数
    tomcat就是在bin目录下的catalina.sh中的头部可以加入JVM参数:
    JAVA_OPTS="$JAVA_OPTS -server -Xms4096m -Xmx6144m -XX:PermSize=256m -XX:MaxPermSize=2048m"

11.被哪些变量引用的对象是不能回收的?

JVM中使用了一种可达性分析算法来判定哪些对象是可以被回收的,哪些对象是不可以被回收的。这个算法的意思,就是说对每个对象,都分析一下有谁在引用他,然后一层一层往上去判断,看是否有一个GC Roots。方法的局部变量、类的静态变量都是GC Roots,所以,只要你的对象被方法的局部变量、类的静态变量给引用了,就不会回收他们。
Java里有不同的引用类型,分别是强引用、软引用、弱引用和虚引用。
只要是强引用的类型,那么垃圾回收的时候绝对不会去回收这个对象的。
正常情况下垃圾回收是不会回收软引用对象的,但是如果你进行垃圾回收之后,发现内存空间还是不够存放新的对象,内存都快溢出了,此时就会把这些软引用对象给回收掉,哪怕他被变量引用了,但是因为他是软引用,所以还是要回收。
弱引用就跟没引用是类似的,如果发生垃圾回收,就会把这个对象回收掉。
虚引用,这个大家其实暂时忽略他也行,因为很少用。
其实这里比较常用的,就是强引用和软引用,强引用就是代表绝对不能回收的对象,软引用就是说有的对象可有可无,如果内存实在不够了,可以回收他。
finalize()方法的作用
假设没有GC Roots引用的对象,是一定立马被回收吗?其实不是的,这里有一个finalize()方法可以拯救他自己,看下面的代码。

finalize()方法的作用.png

假设有一个ReplicaManager对象要被垃圾回收了,那么假如这个对象重写了Object类中的finialize()方法,此时会先尝试调用一下他的finalize()方法,看是否把自己这个实例对象给了某个GC Roots变量,比如说代码中就给了ReplicaManager类的静态变量。如果重新让某个GC Roots变量引用了自己,那么就不用被垃圾回收了。不过说实话,这个东西没必要过多解读,因为其实平时很少用,就是给大家梳理出来这些细节,让大家清楚而已。

12.新生代进行垃圾回收的算法

  1. 一种不太好的垃圾回收思路—标记-清除算法
    标记出哪些对象是可以被垃圾回收的,然后就直接对那块内存区域中的对象进行垃圾回收,把内存空出来。

    标记-清除算法.png

    缺点:形成大量内存碎片,造成内存浪费,可能因为内存碎片太多的缘故,虽然所有的内存碎片加起来其实有很大的一块内存,但是因为这些内存都是碎片式分散的,所以导致没有一块完整的足够的内存空间来分配新的对象。
    无法分配新的对象.png

  2. 一种合理的垃圾回收思路—复制算法
    所谓的“复制算法“,把新生代内存划分为两块内存区域,然后只使用其中一块内存待那块内存快满的时候,就把里面的存活对象一次性转移到另外一块内存区域,保证没有内存碎片,接着一次性回收原来那块内存区域的垃圾对象,再次空出来一块内存区域。两块内存区域就这么重复着循环使用。

复制算法.png
分配新对象.png
清空垃圾对象.png

缺点:从始至终,就只有一半的内存可以用,这样的算法显然对内存的使用效率太低了。

  1. 复制算法的优化:Eden区和Survivor区
    把新生代内存区域划分为三块:1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每一块Survivor区就100MB内存。平时可以使用的,就是Eden区和其中一块Survivor区,那么相当于就是有900MB的内存是可以使用的。


    image.png

    但是刚开始对象都是分配在Eden区内的,如果Eden区快满了,此时就会触发垃圾回收,此时就会把Eden区中的存活对象都一次性转移到一块空着的Survivor区。接着Eden区就会被清空,然后再次分配新对象到Eden区里,然后就会如上图所示,Eden区和一块Survivor区里是有对象的,其中Survivor区里放的是上一次Minor GC后存活的对象。如果下次再次Eden区满,那么再次触发Minor GC,就会把Eden区和放着上一次Minor GC后存活对象的Survivor区内的存活对象,转移到另外一块Survivor区去。


    image.png

    接着新对象继续分配在Eden区和另外那块开始被使用的Survivor区,然后始终保持一块Survivor区是空着的,就这样一直循环使用这三块内存区域。
    这么做最大的好处,就是只有10%的内存空间是被闲置的,90%的内存都被使用上了无论是垃圾回收的性能,内存碎片的控制,还是说内存使用的效率,都非常的好。

13.对象是如何进入老年代的?

1. 躲过15次GC之后进入老年代
对象在新生代里躲过一次GC被转移到一块Survivor区域中,此时他的年龄就会增长一岁,默认的设置下,当对象的年龄达到15岁的时候,也就是躲过15次GC的时候,他就会转移到老年代里去。
这个具体是多少岁进入老年代,可以通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁。
2. 动态对象年龄判断
他的大致规则就是,假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了。
实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代。
3. 大对象直接进入老年代
有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。他的意思就是,如果你要创建一个大于这个大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个大对象放到老年代里去。压根儿不会经过新生代。之所以这么做,就是要避免新生代里出现那种大对象,然后屡次躲过GC,还得把他在两个Survivor区域里来回复制多次之后才能进入老年代,那么大的一个对象在内存里来回复制,不是很耗费时间吗?
4. Minor GC后的对象太多无法放入Survivor区直接转移到老年代

无法放入Survivor区.png

直接转移到老年代.png

14.老年代空间分配担保规则(老年代何时进行垃圾回收

)?
在执行任何一次Minor GC之前,JVM会先检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,因为即使Minor GC之后所有对象都存活,Survivor区放不下了,也可以转移到老年代去。
假如Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,就会看一个“-XX:-HandlePromotionFailure”的参数是否设置了,如果有这个参数,那么就会继续尝试进行下一步判断。
下一步判断,就是看看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。

两次判断.png

如果上面那个步骤判断失败了,或者是“-XX:-HandlePromotionFailure”参数没设置,此时就会直接触发一次“Full GC”,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC。
如果上面两个步骤都判断成功了,那么就是说可以冒点风险尝试一下Minor GC。此时进行Minor GC有几种可能。
第一种可能,Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可。
第二种可能,Minor GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可。
第三种可能,很不幸,Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触发一次“Full GC”。
Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。因为这个时候必须得把老年代里的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代里面。
如果要是Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的“OOM”内存溢出了。

简单来说,一句话总结,对老年代触发垃圾回收的时机,一般就是两个:
要不然是在Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需要提前触发Full GC然后再带着进行Minor GC;要不然是在Minor GC之后,发现剩余对象太多放入老年代都放不下了。

15.老年代垃圾回收算法—标记整理算法

首先标记出来老年代当前存活的对象,这些对象可能是东一个西一个的。


回收前.png

接着会让这些存活对象在内存里进行移动,把存活对象尽量都挪动到一边去,让存活对象紧凑的靠在一起,避免垃圾回收过后出现过多的内存碎片,然后再一次性把垃圾对象都回收掉。


回收后.png

老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况。
所谓JVM优化,就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。

老年代为什么不采用复制算法?
老年代存活对象太多了,如果采用复制算法,每次都挪动可能90%的存活对象,这就不合适了。所以采用先把存活对象挪动到一起紧凑一些,然后回收垃圾对象的方式。

16.JVM的痛点:Stop the World

平时使用JVM最大的痛点,其实就是在垃圾回收的这个过程,因为在垃圾回收的时候,尽可能要让垃圾回收器专心致志的干工作,不能随便让我们写的Java系统继续对象了,所以此时JVM会在后台
直接进入“Stop the World”状态。也就是说,他会直接停止我们写的Java系统的所有工作线程,让我们写的代码不再运行!然后让垃圾回收线程可以专心致志的进行垃圾回收的工作。接着一旦垃圾回收完毕,就可以继续恢复我们写的Java系统的工作线程的运行了,然后我们的那些代码就可以继续运行。


Stop the World.png

垃圾回收完毕.png

现在就很清晰“Stop the World”会对系统造成的影响了, 假设我们的Minor GC要运行100ms,那么可能就会导致我们的系统直接停顿100ms不能处理任何请求,在这100ms期间用户发起的所有请求都会出现短暂的卡顿,因为系统的工作线程不在运行,不能处理请求。
假设你开发的是一个Web系统,那么可能导致你的用户从网页或者APP上点击一个按钮,然后平时只要几十ms就可以返回响应了,现在因为你的Web系统的JVM正在执行Minor GC,暂停了所有的工作线程,导致你的请求过来到响应返回,这次需要等待几百毫秒。

17.最常用的新生代垃圾回收器:ParNew

新生代的ParNew垃圾回收器主打的就是多线程垃圾回收机制,另外一种Serial垃圾回收器主打的是单线程垃圾回收,他们俩都是回收新生代的,唯一的区别就是单线程和多线程的区别,但是垃圾回收算法是完全一样的。


ParNew垃圾回收器.png

在启动系统的时候如果要指定使用ParNew垃圾回收器,是用什么参数呢?
使用“-XX:+UseParNewGC”选项,只要加入这个选项,JVM启动之后对新生代进行垃圾回收的,就是ParNew垃圾回收器了。
ParNew垃圾回收器默认情况下的线程数量
一旦指定了使用ParNew垃圾回收器之后,他默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样的。
但是如果你一定要自己调节ParNew的垃圾回收线程数量,也是可以的,使用“-XX:ParallelGCThreads”参数即可,通过他可以设置线程的数量,但是建议一般不要随意动这个参数。

18.老年代垃圾回收器CMS

CMS在执行一次垃圾回收的过程一共分为4个阶段:
初始标记、并发标记、重新标记、并发清理

  1. 所谓的“初始标记”,就是标记出来所有GC Roots直接引用的对象,这个阶段会让系统的工作线程全部停止,进入“Stop the World”状态。虽然说要造成“Stop the World”暂停一切工作线程,但是其实影响不大,因为他的速度很快,仅仅标记GC Roots直接引用的那些对象罢了。
    初始标记.png
  2. 第二个阶段,就是对老年代所有对象进行GC Roots追踪,其实是最耗时的,他需要追踪所有对象是否从根源上被GC Roots引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,系统程序会不停的工作,他可能会各种创建出来新的对象,部分对象可能成为垃圾,其实这个阶段不会对系统运行造成影响的。


    并发标记.png
  3. 在第二阶段里,你一边标记存活对象和垃圾对象,一边系统在不停运行创建新对象,让老对象变成垃圾所以第二阶段结束之后,绝对会有很多存活对象和垃圾对象,是之前第二阶段没标记出来的。所以此时进入第三阶段,要继续让系统程序停下来,再次进入“Stop the World”阶段。然后重新标记下在第二阶段里新创建的一些对象,还有一些已有对象可能失去引用变成垃圾的情况


    重新标记.png
  4. 并发清理这个阶段就是让系统程序随意运行,然后他来清理掉之前标记为垃圾的对象即可。这个阶段其实是很耗时的,因为需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行。
    并发清理.png

    对CMS的垃圾回收机制进行性能分析
    其实大家看完CMS的垃圾回收机制之后,就会发现,他已经尽可能的进行了性能优化了。因为最耗时的,其实就是对老年代全部对相关进行GC Roots追踪,标记出来到底哪些可以回收,然后就是对各种垃圾对象从内存里清理掉,这是最耗时的。但是他的第二阶段和第四阶段,都是和系统程序并发执行的,所以基本这两个最耗时的阶段对性能影响不大。只有第一个阶段和第三个阶段是需要“Stop the World”的,但是这两个阶段都是简单的标记而已,速度非常的快,所以基本上对系统运行响应也不大。
    CMS的垃圾回收的问题以及参数设置
    1.CPU资源紧张
    CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4。我们用最普通的2核4G机器和4核8G机器来计算一下,假设是2核CPU,本来CPU资源就有限,结果此时CMS还会有个“(2 + 3) / 4”= 1个垃圾回收线程,去占用宝贵的一个CPU。所以其实CMS这个并发垃圾回收的机制,第一个问题就是会消耗CPU资源。
    2.Concurrent Mode Failure问题
    在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象,但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是“浮动垃圾”。大家看下图那个红圈画的地方,那个对象就是在并发清理期间,系统程序可能先把某些对象分配在新生代,然后可能触发了一次Minor GC,一些对象进入了老年代,然后短时间内又没人引用这些对象了。
    浮动垃圾.png

    因为他虽然成为了垃圾,但是CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候才会回收他们。所以为了保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC。
    “-XX:CMSInitiatingOccupancyFaction”参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。也就是说,老年代占用了92%空间了,就自动进行CMS垃圾回收,预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代中。
    那么如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,此时会如何?
    这个时候,会发生Concurrent Mode Failure,就是说并发垃圾回收失败了,我一边回收,你一边把对象放入老年代,内存都不够了。
    此时就会自动用“Serial Old”垃圾回收器替代CMS,就是直接强行把系统程序“Stop the World”,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生,然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。所以在生产实践中,这个自动触发CMS垃圾回收的比例需要合理优化一下,避免“Concurrent Mode Failure”问题。
    3.内存碎片问题
    老年代的CMS采用“标记-清理”算法,每次都是标记出来垃圾对象,然后一次性回收掉,这样会导致大量的内存碎片产生。如果内存碎片太多,会导致后续对象进入老年代找不到可用的连续内存空间了,然后触发Full GC。所以CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Full GC。
    CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了
    他意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片。
    还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理。

19.G1垃圾回收器

G1的核心设计思路:G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象。

Region的划分.png

Region可能属于新生代也可能属于老年代
在G1中,每一个Region时可能属于新生代,但是也可能属于老年代的,刚开始Region可能谁都不属于,然后接着就分配给了新生代,然后放了很多属于新生代的对象,接着就触发了垃圾回收这个Region。
回收新生代Region.png

然后下一次同一个Region可能又被分配了老年代了,用来放老年代的长生存周期的对象。
Region变为老年代.png

到底有多少个Region呢?每个Region的大小是多大呢?
其实这个默认情况下自动计算和设置的,我们可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。然后JVM启动的时候一旦发现你使用的是G1垃圾回收器,可以使用“-XX:+UseG1GC”来指定使用G1垃圾回收器,此时会自动用堆大小除以2048,因为JVM最多可以有2048个Region,然后Region的大小必须是2的倍数,比如说1MB、2MB、4MB之类的。
比如说堆大小是4G,那么就是4096MB,此时除以2048个Region,每个Region的大小就是2MB。大概就是这样子来决定Region的数量和大小的,大家一般保持默认的计算方式就可以,如果通过手动方式来指定,则是“-XX:G1HeapRegionSize”
刚开始的时候,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,这个是可以通过“-XX:G1NewSizePercent”来设置新生代初始占比的,其实维持这个默认值即可。
因为在系统运行中,JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”。而且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态的。

新生代还有Eden和Survivor的概念吗?
没错,其实在G1中虽然把内存划分为了很多的 Region,但是其实还是有新生代、老年代的区分,而且新生代里还是有Eden和Survivor的划分的。

G1的新生代垃圾回收
随着不停的在新生代的Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%。
一旦新生代达到了设定的占据堆内存的最大大小60%,比如都有1200个Region了,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区还占满了对象,这个时候还是会触发新生代的GC,G1就会用之前说过的复制算法来进行垃圾回收,进入一个“Stop the World”状态,然后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden对应的Region中的垃圾对象。但是这个过程跟之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms。
那么G1就会通过之前说的,对每个Region追踪回收他需要多少时间,可以回收多少对象来选择回收一部分的Region,保证GC停顿时间控制在指定范围内,尽可能多的回收掉一些对象。

对象什么时候进入老年代?
(1)对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,他就会进入老年代;
(2)动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%。

大对象的独立Region存放和回收
G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的Region中而且一个大对象如果太大,可能会横跨多个Region来存放。

image.png

大对象不属于新生代和老年代,其实新生代、老年代在回收的时候,会顺带带着大对象Region一起回收,所以这就是在G1内存模型下对大对象的分配和回收的策略。

什么时候触发新生代+老年代的混合垃圾回收?
G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,他的默认值是45%。意思就是说,如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。比如按照我们之前说的,堆内存有2048个Region,如果老年代占据了其中45%的Region,也就是接近1000个Region的时候,就会开始触发一个混合回收。

新生代+老年代的混合垃圾回收.png

G1垃圾回收的过程

  1. 初始标记:先停止系统程序的运行,然后对各个线程栈内存中的局部变量代表的GC Roots,以及方法区中的类静态变量代表的GCRoots,进行扫描,标记出来他们直接引用的那些对象。

    初始标记.png

  2. 并发标记:这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象。JVM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。

    并发标记.png

  3. 最终标记:这个阶段会进入“Stop the World”,系统程序是禁止运行的,但是会根据并发标记 阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象。

    最终标记.png

  4. 混合回收:这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内。

    混合回收.png

而且需要在这里有一点认识,其实老年代对堆内存占比达到45%的时候,触发的是“混合回收”
也就是说,此时垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象。

G1垃圾回收器的一些参数

  1. “-XX:G1MixedGCCountTarget”
    最后一个阶段混合回收的时候,其实会停止所有程序运行,所以说G1是允许执行多次混合回收。
    比如先停止工作,执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。
    有一些参数可以控制这个,比如“-XX:G1MixedGCCountTarget”参数,就是在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次,意味着最后一个阶段,先停止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统运行,混合回收一些Region,反复8次。为什么要反复回收多次呢?因为你停止系统一会儿,回收掉一些Region,再让系统运行一会儿,然后再次停止系统一会儿,再次回收掉一些Region,这样可以尽可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下。

  2. “-XX:G1HeapWastePercent”
    还有一个参数,就是“-XX:G1HeapWastePercent”,默认值是5%,意思就是说,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他
    Region,然后这个Region中的垃圾对象全部清理掉。这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

    image.png

G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后,再进行内存碎片的整理。

  1. “-XX:G1MixedGCLiveThresholdPercent”
    还有一个参数,“-XX:G1MixedGCLiveThresholdPercent”,他的默认值是85%,意思就是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收,否则要是一个Region的存活对象多余85%,你还回收他干什么?这个时候要把85%的对象都拷贝到别的Region,这个成本是很高的。

回收失败时的Full GC
如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去,此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发 一次失败。一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。

20.G1新生代gc如何优化?

对于G1而言,我们首先应该给整个JVM的堆区域足够的内存,比如我们在这里就给了JVM超过5G的内存,其中堆内存有4G的内存。接着就应该合理设置“-XX:MaxGCPauseMills”参数,如果这个参数设置的小了,那么说明每次gc停顿时间可能特别短,此时G1一旦发现你对几十个Region占满了就立即触发新生代gc,然后gc频率特别频繁,虽然每次gc时间很短。比如说30秒触发一次新生代gc,每次就停顿30毫秒。
如果这个参数设置大了呢?那么可能G1会允许你不停的在新生代理分配新的对象,然后积累了很多对象了,再一次性回收几百个Region,此时可能一次GC停顿时间就会达到几百毫秒,但是GC的频率很低。比如说30分钟才触发一次新生代GC,但是每次停顿500毫秒。
所以这个参数到底如何设置,需要结合后续给大家讲解的系统压测工具、gc日志、内存分析工具结合起来进行考虑,尽量让系统的gc频率别太高,同时每次gc停顿时间也别太长,达到一个理想的合理值。

21.G1 mixed gc如何优化?

对于这个mixed gc的触发,大家都知道是老年代在堆内存里占比超过45%就会触发。
大家之前都很清楚了年轻代的对象进入老年代的几个条件了,要不然是新生代gc过后存活对象太多没法放入Survivor区域,要不然是对象年龄太大,要不然是动态年龄判定规则。其中尤其关键的,就是新生代gc过后存活对象过多无法放入Survivor区域,以及动态年龄判定规则,这两个条件尤其可能让很多对象快速进入老年代,一旦老年代频繁达到占用堆内存45%的阈值,那么就会频繁触发mixed gc。所以mixed gc本身很复杂,很多参数可以优化,但是优化mixed gc的核心不是优化他的参数,而是跟我们之前分析的思路一样,尽量避免对象过快进入老年代,尽量避免频繁触发mixed gc,就可以做到根本上优化mixed gc了。
那么G1里面跟之前的ParNew+CMS的组合是不同的,我们到底应该如何来优化参数呢?
其实核心的点,还是“-XX:MaxGCPauseMills”这个参数。大家可以想一下,假设你“-XX:MaxGCPauseMills”参数设置的值很大,导致系统运行很久,新生代可能都占用了堆
内存的60%了,此时才触发新生代gc。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。或者是你新生代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。
所以这里核心还是在于调节“-XX:MaxGCPauseMills”这个参数的值,在保证他的新生代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc。
至于到底如何优化这个参数,一切都要结合后续大量工具的讲解和实操演练了,到这里为止,至少大家对原理性的东西都很了解了。

22.年轻代gc到底多久一次对系统影响不大?

其实通常来说是不大的,不知道大家发现没有,其实年轻代gc几乎没什么好调优的,因为他的运行逻辑非常简单,就是Eden一旦满了,无法放新对象就触发一次gc。
一般来说,真要说对年轻代的gc进行调优,只要你给系统分配足够的内存即可,核心点还是在于堆内存的分配、新生代内存的分配内存足够的话,通常来说系统可能在低峰时期在几个小时才有一次新生代gc,高峰期最多也就几分钟一次新生代gc。而且一般的业务系统都是部署在2核4G或者4核8G的机器上,此时分配给堆的内存不会超过3G,给新生代中的Eden区的内存也就1G左右。
而且新生代采用的复制算法效率极高,因为新生代里存活的对象很少,只要迅速标记出这少量存活对象,移动到Survivor区,然后回收掉其他全部垃圾对象即可,速度很快。
很多时候,一次新生代gc可能也就耗费几毫秒,几十毫秒。大家设想一下,假如说你的系统运行着,然后每隔几分钟或者几十分钟执行一次新生代gc,系统卡顿几十毫秒,就这期间的请求会卡顿几十毫秒,几乎用户都是无感知的,所以新生代gc一般基本对系统性能影响不大。

23.什么时候新生代gc对系统影响很大?

简单,当你的系统部署在大内存机器上的时候,比如说你的机器是32核64G的机器,此时你分配给系统的内存有几十个G,新生代的Eden区可能30G~40G的内存。比如类似Kafka、Elasticsearch之类的大数据相关的系统,都是部署在大内存的机器上的,此时如果你的系统负载非常的高,对于大数据系统是很有可能的,比如每秒几万的访问请求到Kafka、Elasticsearch上去。
那么可能导致你Eden区的几十G内存频繁塞满要触发垃圾回收,假设1分钟会塞满一次。然后每次垃圾回收要停顿掉Kafka、Elasticsearch的运行,然后执行垃圾回收大概需要几秒钟,此时你发现,可能每过一分钟,你的系统就要卡顿几秒钟,有的请求一旦卡死几秒钟就会超时报错,此时可能会导致你的系统频繁出错。
如何解决这种几十G的大内存机器的新生代GC过慢的问题呢?
用G1垃圾回收器,针对G1垃圾回收器,可以设置一个期望的每次GC的停顿时间,比如我们可以设置一个20ms。
那么G1基于他的Region内存划分原理,就可以在运行一段时间之后,比如就针对2G内存的Region进行垃圾回收,此时就仅仅停顿20ms,然后回收掉2G的内存空间,腾出来了部分内存,接着还可以继续让系统运行。
G1天生就适合这种大内存机器的JVM运行,可以完美解决大内存垃圾回收时间过长的问题。

24.要命的频繁老年代gc问题

老年代gc通常来说都很耗费时间,无论是CMS垃圾回收器还是G1垃圾回收器,因为比如说CMS就要经历初始标记、并发标记、重新标记、并发清理、碎片整理几个环节,过程非常的复杂,G1同样也是如此。
通常来说,老年代gc至少比新生代gc慢10倍以上,比如新生代gc每次耗费200ms,其实对用户影响不大,但是老年代每次gc耗费2s,那可能就会导致老年代gc的时候用户发现页面上卡顿2s,影响就很大了。
所以一旦你因为jvm内存分配不合理,导致频繁进行老年代gc,比如说几分钟就有一次老年代gc,每次gc系统都停顿几秒钟,那简直对你的系统就是致命的打击。此时用户会发现页面上或者APP上经常性的出现点击按钮之后卡顿几秒钟。
其实说白了,系统真正最大的问题,就是因为内存分配、参数设置不合理,导致你的对象频繁的进入老年代,然后频繁触发老年代gc,导致系统频繁的每隔几分钟就要卡死几秒钟

25.什么是内存溢出?内存溢出会在哪些区域发生?

image.png

内存溢出可能会发生在:

  1. Metaspace;
  2. Java虚拟机栈;
  3. 堆内存。
    除了程序计数器,其它区域都有可能发生OOM。

Metaspace区域是如何因为类太多而发生内存溢出的?
以下两个参数就是用来设置Metaspace区域大小的:
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
一旦JVM不停地加载类,加载了很多很多的类,然后Metaspace区域放满了,当Metaspace区域满就会触发Full GC,Full GC会带着一块进行Old GC就是回收老年代的,也会带着回收年轻代的Young GC,当然,Full GC的时候,必然会尝试回收Metaspace区域中的类。

MetaSpace内存溢出.png

那么什么样的类才是可以被回收的呢?
这个条件是相当的苛刻,包括不限于以下一些:比如这个类的类加载器先要被回收,比如这个类的所有对象实例都要被回收,等等。
所以一旦你的Metaspace区域满了,未必能回收掉里面很多的类那么一旦回收不了多少类,此时你的JVM还在拼命的加载类放到Metaspace里去,你觉得此时会发生什么事情?
显而易见,一旦你尝试回收了Metaspace中的类之后发现还是没能腾出来太多空间,此时还要继续往Metaspace中塞入更多的类,直接就会引发内存溢出的问题。因为此时Metaspace区域的内存空间不够了。一旦发生了内存溢出就说明JVM已经没办法继续运行下去了,此时可能你的系统就直接崩溃了,这就是Metaspace区域发生内存溢出的一个根本的原理。

到底什么情况下会发生Metaspace内存溢出?

  1. 很多工程师他不懂JVM的运行原理,在上线系统的时候对Metaspace区域直接用默认的参数,即根本不设置其大小这会导致默认的Metaspace区域可能才几十MB而已,此时对于一个稍微大型一点的系统,因为他自己有很多类,还依赖了很多外部的jar包有有很多的类,几十MB的Metaspace很容易就不够了;
  2. 很多人写系统的时候会用cglib之类的技术动态生成一些类,一旦代码中没有控制好,导致你生成的类过于多的时候,就很容易把Metaspace给塞满,进而引发内存溢出。

对于第一种问题,通常来说,有经验的工程师上线系统往往会设置对应的Metaspace大小,推荐的值在512MB那样,一般都是足够的。

无限制的调用方法是如何让线程的栈内存溢出的?
可以手动设置每个线程的虚拟机栈的内存大小的,一般来说现在默认都是给设置1MB。假设你不停的让这个线程去调用各种方法,然后不停的把方法调用的栈桢压入栈中,是不是就会不断的占用这个线程1MB的栈内存?那么如果不停的让线程调用方法,不停的往栈里放入栈桢,此时终有一个时刻,大量的栈桢会消耗完毕这个1MB的线程栈内存,最终就会导致出现栈内存溢出的情况

栈内存溢出.png

通常而言,哪怕你的线程的虚拟机栈内存就128KB,或者256KB,通常都是足够进行一定深度的方法调用的。但是如果说你要是走一个递归方法调用,一个线程就会不停的调用同一个方法,即使是同一个方法,每一次方法调用也会产生一个栈桢压入栈里,比如说对sayHello()进行100次调用,那么就会有100个栈桢压入中。所以如果疯狂的运行上述代码,就会不停的将sayHello()方法的栈桢压入栈里,最终一定会消耗掉线程的栈内存,引发内存溢出。所以一般来说,其实引发栈内存溢出,往往都是代码里写了一些bug才会导致的,正常情况下发生的比较少。

对象太多了导致堆内存实在是放不下,只能内存溢出!
有限的内存中放了过多的对象,而且大多数都是存活的,此时即使GC过后还是大部分都存活,所以要继续放入更多对象已经不可能了,此时只能引发内存溢出问题。

堆内存溢出.png

所以一般来说发生内存溢出有两种主要的场景:

  1. 系统承载高并发请求,因为请求量过大,导致大量对象都是存活的,所以要继续放入新的对象实在是不行了,此时就会引发OOM系统崩溃;
  2. 系统有内存泄漏的问题,就是莫名其妙弄了很多的对象,结果对象都是存活的,没有及时取消对他们的引用,导致触发GC还是无法回收,此时只能引发内存溢出,因为内存实在放不下更多对象了。
    因此总结起来,一般引发OOM,要不然是系统负载过高,要不然就是有内存泄漏的问题。

27.用CGLIB动态生成类的代码演示Metaspace区域内存溢出的场景

首先设置MetaSpace区域大小:
-XX:MetaspaceSize=10m
-XX:MaxMetaspaceSize=10m

演示代码:

public class Demo8 {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Car.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    if (method.getName().equals("run")) {
                        System.out.println("启动汽车之前,先进行安全检查...");
                        return methodProxy.invoke(o, objects);
                    } else {
                        return methodProxy.invoke(o, objects);
                    }
                }
            });

            Car car = (Car) enhancer.create();
            car.run();
        }
    }

    static class Car {

        public void run() {
            System.out.println("汽车启动,开始行驶...");
        }
    }
}

通过一个while(true)循环不断动态创建Car的子类,每创建一个代理子类,该类信息都会被放到MetaSpace区域中,因为MetaSpace区域设定为只有10M,所以很快就会出现OOM异常。

27.用递归调用方法的代码演示栈内存溢出的场景

首先设置栈内存为:
-XX:ThreadStackSize=1m

演示代码:

public class Demo9 {

    private static long counter = 0;

    public static void main(String[] args) {
        work();
    }

    private static void work() {
        System.out.println("目前是第" + ++counter + "次方法调用.");
        work();
    }
}

上面的代码非常简单,就是work()方法调用自己,进入一个无限制的递归调用,陷入死循环,也就是说在main线程的栈中,会不停的压入work()方法调用的栈桢,直到1MB的内存空间耗尽。


栈内存溢出.png

27.通过代码演示堆内存溢出的场景

首先设置堆内存大小:
-Xms10m
-Xmx10m

演示代码:

public class Demo10 {

    private static long counter = 0;

    public static void main(String[] args) {
        ArrayList<Object> objects = new ArrayList<>();
        while (true) {
            objects.add(new Object());
            System.out.println("当前创建了第" + (++counter) + "个对象.");
        }
    }
}

代码很简单,就是在一个while循环里不停的创建对象,而且对象全部都是放在List里面被引用的,也就是不能被回收。试想一下,如果你不停的创建对象,Eden区满了,他们全部存活会全部转移到老年代,反复几次之后老年代满了。然后Eden区再次满了,ygc后存活对象再次进入老年代,此时老年代先full gc,但是回收不了任何对象,因此ygc后的存活对象就一定是无法进入老年代的。


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

推荐阅读更多精彩内容