阿里P6之四JVM+GC解析

个人专题目录


4 JVM+GC解析

4.1 回顾

4.1.1 JVM内存结构

JVM体系概述

1558690006345.png

Java8以后的JVM

1558690061817.png

4.1.2 GC作用域

方法区,堆

4.1.3 常见的垃圾回收算法

引用计数

1558690144067.png

复制

1558690188814.png

标记清除

1558690225969.png

标记整理

1558690275674.png

4.2 题目1

4.2.1 JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots

什么是垃圾

简单来说就是内存中已经不再被使用到的空间就是垃圾

要进行垃圾回收,如何判断一个对象是否可以被回收?

引用计数法

    Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。

因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1。

    任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。

枚举根节点做可达性分析(根搜索路径)

    为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。
1558840571793.png

所谓“GC roots”或者说tracing GC的“根集合”就是一组必须活跃的引用

基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明些对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的对象就被判定为存活;没有被遍历到的就自然被决定死亡。

1559790714426.png

Java中可以作为GC Roots的对象

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

4.2.2 你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值

JVM的参数类型

标配参数
  • -version
  • -help
  • java -showversion
X参数(了解)
  • -Xint 解释执行
  • -Xcomp 第一次使用就编译成本地代码
  • -Xmixed 混合模式
XX参数
  • Boolean类型
    • 公式
      • -XX:+或者 - 某个属性值
      • +表示开启,-表示关闭
    • Case
      • 是否打印GC收集细节
        • -XX:-PrintGCDetails
          • jps -l
          • jinfo -flag PrintGCDetails 进程编号
        • -XX:+PrintGCDetails
          • jps -l
          • jinfo -flag PrintGCDetails 进程编号
      • 是否使用串行垃圾收集器
        • -XX:-UseSerialGC
        • -XX:+UseSerialGC
  • KV设值类型
    • 公式
      • -XX:属性key=属性值value
    • Case
      • -XX:MetaspaceSize=128m
      • -XX:MaxTenuringThreshold=15
  • jinfo类型,如何查看当前运行程序的配置
    • 公式(jinfo -flag 配置项 进程编号)
    • Case
      • jinfo -flag InitialHeapSize 进程编号
      • jinfo -flags 进程编号
      • jinfo -flag UseParallelGC 进程编号
  • 其它
    • 两个经典参数:-Xms和-Xmx
    • 如何理解
      • -Xms 等价于 -XX:InitialHeapSize
      • -Xmx 等价于 -XX:MaxHeapSize

盘点查看JVM默认值

  • -XX:+PrintFlagsInitial
    • 主要查看初始默认
    • 公式
      • java -XX:+PrintFlagsInitial -version
  • -XX:+PrintFlagsFinal
    • 主要查看修改更新
    • 公式
      • java -XX:+PrintFlagsFinal -version
  • PrintFlagsFinal举例,运行java命令的同时打印出参数
    • java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m Test
  • -XX:+PrintCommandLineFlags

4.2.3 你平时工作中用过的JVM常用基本配置参数有哪些?

基础知识

在java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。

元空间(java8)与永久代(java7)之间最大的区别在于:永久代使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。

因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

public class HelloGC {
    public static void main(String[] args) {
        long totalMemory = Runtime.getRuntime().totalMemory();//返回Java虚拟机中的内存总量。
        long maxMemory = Runtime.getRuntime().maxMemory();//返回Java虚拟机试图使用的最大内存量。
        System.out.println((totalMemory / (double) 1024 / 1024) + "MB");
        System.out.println((maxMemory / (double) 1024 / 1024) + "MB");
    }
}

常用参数

  • -Xms
    • 初始大小内存,默认为物理内存1/64,等价于-XX:InitialHeapSize
  • -Xmx
    • 最大分配内存,默认为物理内存1/4,等价于-XX:MaxHeapSize
  • -Xss
    • 设置单个线程栈的大小,一般默认为512k~1024k,等价于-XX:ThradStackSize
  • -Xmn
    • 设置年轻代大小
  • -XX:MetaspaceSize
    • 设置元空间大小
      • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
      • -Xms10m -Xmx10m -XX:MetespaceSize=1024m -XX:+PrintFlagsFinal
  • -XX:+PrintGCDetails
    • 输出详细GC收集日志信息
    • GC
    • FullGC
  • -XX:SurvivorRatio
    • 设置新生代中eden和s0/s1空间的比例,默认-XX:SurvivorRatio=8,Eden:s0:s1=8:1:1,假如-XX:SurvivorRatio=4,Eden:s0:s1=4:1:1,SurvivorRatio值就是设置eden区的比例占多少,s0/s1相同
  • -XX:NewRatio
    • 配置年轻代与老年代在堆结构的占比,默认-XX:NewRatio=2,新生代占1,老年代占2,年轻代占整个堆的1/3;假如-XX:NewRatio=4新生代占1,老年代占4,年轻代占整个堆的1/5,NewRatio值就是设置老年代的占比,剩下的1给新生代
  • -XX:MaxTenuringThreshold
    • 设置垃圾最大年龄,默认值为15,有效值在0到15之间,-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年经代即被回收的概率。
1558875048259.png

4.2.4 强引用、软引用、弱引用、虚引用分别是什么?

1558934719418.png
  • 整体架构

  • 强引用(默认支持模式)

    • 当内存不足,JVM开始垃圾回收,对于强引用对象,就算是出现了OOM也不会对该对象进行回收,死都不收。强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃圾收集了(当然具体回收时机还是要看垃圾收集策略)。
  public class StrongReferenceDemo {
      public static void main(String[] args) {
          Object obj1 = new Object();//这样定义的默认就是强引用
          Object obj2 = obj1;//obj2引用赋值
          obj1 = null;//置空
          System.gc();
          System.out.println(obj2);
      }
  }
  • 软引用

    • 软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。对于只有软引用的对象来说,当系统内存充足时它不会被回收,当系统内存不足时会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存就有和到软引用,内存够用的时候就保留,不够用就回收。
  public class SoftReferenceDemo {
      /**
       * 内存够用的时候就保留,不够用就回收
       */
      public static void softRef_Memory_Enough() {
          Object o1 = new Object();
          SoftReference<Object> softReference = new SoftReference<>(o1);
          System.out.println(o1);
          System.out.println(softReference.get());
          o1 = null;
          System.gc();
          System.out.println(o1);
          System.out.println(softReference.get());
      }
  
      /**
       * JVM配置,故意产生大对象并配置小的内存,让它内存不够用了导致OOM,看软引用的回收情况
       * -Xms5m -Xmx5m -XX:+PrintGCDetails
       */
      public static void softRef_Memory_NotEnough() {
          Object o1 = new Object();
          SoftReference<Object> softReference = new SoftReference<>(o1);
          System.out.println(o1);
          System.out.println(softReference.get());
          o1 = null;
          try {
              byte[] bytes = new byte[30 * 1024 * 1024];
          } catch (Throwable e) {
              e.printStackTrace();
          } finally {
              System.out.println(o1);
              System.out.println(softReference.get());
          }
      }
  
      public static void main(String[] args) {
          softRef_Memory_Enough();
          softRef_Memory_NotEnough();
      }
  }
  • 弱引用

    • 弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
    • 软引用和弱引用的使用场景。 假如有一个应用需要读取大量的本地图片:如果每次读取图片都从硬盘读取则会严重影响性能,如果一次性全部加载到内存中又可能造成内存溢出。此时可以用软引用解决这个问题。设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象占用的空间,从而有效地避免了OOM的问题。Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();
    • 谈谈WeakHashMap
  public class WeakReferenceDemo {
      public static void main(String[] args) {
          Object o1 = new Object();
          WeakReference<Object> weakReference = new WeakReference<>(o1);
          System.out.println(o1);
          System.out.println(weakReference.get());
  
          o1 = null;
          System.gc();
          System.out.println("----------------------");
  
          System.out.println(o1);
          System.out.println(weakReference.get());
  
      }
  }
  
  public class WeakHashMapDemo {
      public static void main(String[] args) {
          myHashMap();
          myWeakHashMap();
      }
  
      private static void myHashMap() {
          HashMap<Integer, String> map = new HashMap<>();
          Integer key = new Integer(1);
          String value = "HashMap";
          map.put(key, value);
          System.out.println(map);
          key = null;
          System.out.println(map);
          System.gc();
          System.out.println(map);
      }
  
      private static void myWeakHashMap() {
          WeakHashMap<Integer, String> map = new WeakHashMap<>();
          Integer key = new Integer(2);
          String value = "WeakHashMap";
          map.put(key, value);
          System.out.println(map);
          key = null;
          System.out.println(map);
          System.gc();
          System.out.println(map);
      }
  }
  • 虚引用

    • 虚引用需要java.lang.ref.PhantomReference类来实现。顾名思义,就是形同虚设,与其它几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue联合实用)。
    • 虚引用的主要作用跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用以实现比finalization机制更灵活的回收操作。
    • 换句话说,设置虚引用关联 唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
    • 引用队列
  public class ReferenceQueueDemo {
      public static void main(String[] args) throws InterruptedException {
          Object o1 = new Object();
          ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
          WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
  
          System.out.println(o1);
          System.out.println(weakReference.get());
          System.out.println(referenceQueue.poll());
  
          o1 = null;
          System.gc();
          Thread.sleep(10);
          
          System.out.println(o1);
          System.out.println(weakReference.get());
          //被回收前需要引用队列保存下
          System.out.println(referenceQueue.poll());
      }
  }
/**
 * java提供了4种引用类型,在垃圾回收的时候,都有各自的特点。
 * ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。
 * 创建引用的时候可以指定关联的队列,当GC释放对象的时候,会将引用加入到引用队列,
 * 如果程序发现某个虚引用已经被加入以引用队列,那么就可以在所引用的对象的内存被回收之前采取必须的行动
 * 这相当于是一种通知机制。
 * 当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通知这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情
 */
public class PhantomReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Object> phantomReference = new PhantomReference<>(o1, referenceQueue);
        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());
        System.out.println("--------------------");
        o1 = null;
        System.gc();
        Thread.sleep(10);

        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());
    }
}
  • GCRoots和四大引用总结
1559791208982.png

4.2.5 请谈谈你对OOM的认识。

  • java.lang.StackOverflowError
public class StackOverflowErrorDemo {
    public static void main(String[] args) {
        stackOverflowError();
    }

    private static void stackOverflowError() {
        //Exception in thread "main" java.lang.StackOverflowError
        stackOverflowError();
    }
}
  • java.lang.OutOfMemoryError:Java heap space
//-Xms10m -Xmx10m
public class JavaHeapSpaceDemo {
    public static void main(String[] args) {
        String str = "xubh";
        while (true) {
            str += str + new Random().nextInt(100000) + new Random().nextInt(999999);
            str.intern();
        }
        //Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    }
}
  • java.lang.OutOfMemoryError:GC overhead limit exceeded
/**
 * JVM参数配置演示
 * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
 * GC回收时间过长会抛出OutOfMemoryError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存
 * 连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出GC overhead limit错误会发生什么情况呢
 * 那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,
 * CPU使用率一直是100%,而GC却没有任何成果。
 * Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
 */
public class GCOverHeadLimitErrorDemo {
    public static void main(String[] args) {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while (true) {
                list.add(String.valueOf(++i).intern());
            }
        } catch (Throwable e) {
            System.out.println(i);
            e.printStackTrace();
        }
    }

}
  • java.lang.OutOfMemoryError:Direct buffer memory
/**
 * JVM参数配置演示
 * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
 * 故障现象:
 * Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
 * 原因:
 * 写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,
 * 它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面DirectByteBuffer对象作为这块内存的引用进行操作。
 * 这样能在一些场景中显著提高性能,因为避免了Java堆和Native堆中来回复制数据。
 * ByteBuffer.allocate(capability) 第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
 * ByteBuffer.allocateDerect(capability) 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。
 * 但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,
 * 这时候堆内存充足,但本地内存可能已经用光了,再次尝试分配本地内存就会出现OutOfMemoryError,程序就直接崩溃了。
 */
public class DirectBufferMemoryDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(sun.misc.VM.maxDirectMemory() / (double) 1024 / 1024 + "MB");
        Thread.sleep(2000);
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8 * 1024 * 1024);
    }
}
  • java.lang.OutOfMemoryError:unable to create new native thread
    • ulimit -u
    • vim /etc/security/limits.d/90-nproc.conf(除了root用户,其它都限制1024)
/**
 * 高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError:unable to create new native thread
 * 准确的讲该native thread 异常与对应的平台有关
 * <p>
 * 导致原因:
 * 1、你的应用程序创建了太多的线程了,一个应用进程创建多个线程,超过系统承载极限
 * 2、你的服务器并不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024个,
 * 你的应用创建超过这个数量,就会报java.lang.OutOfMemoryError:unable to create new native thread
 * <p>
 * 解决方案:
 * 1、想办法降低应用程序创建线程的数量,分析应用程序是否需要创建这么多线程,修改代码优化。
 * 2、对于有的应用,确实需要大量线程,可以通过修改linux服务器配置,扩大linux默认限制。
 */
public class UnableCreateNewThreadDemo {
    public static void main(String[] args) {
        for (int i = 1; ; i++) {
            System.out.println(i);
            new Thread(() -> {
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "" + i).start();
        }
    }
}
  • java.lang.OutOfMemoryError:Metaspace
    • 使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize约20m
/**
 * java.lang.OutOfMemoryError: Metaspace
 * JVM参数:
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 * java8及以后版本使用metaspace替代永久代。
 * Metaspace是方法区在Hotspot中的实现,它与持久代最大区别在于:Metaspace并不在虚拟机内存中而是在本地内存,被存储在民做
 * Metaspace的native memory
 * <p>
 * 永久代存放了以下信息:
 * 虚拟机加载的类信息
 * 常量池
 * 静态变量
 * 即时编译后的代码
 * <p>
 * 模拟让类占据的空间超过Metaspace指定的空间大小
 */
public class MetaspaceOOMDemo {
    static class OOMTest {

    }

    public static void main(String[] args) {
        int i = 0;
        try {
            while (true) {
                i++;
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMTest.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o, args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            System.out.println("运行多少次发生了异常:" + i);
            e.printStackTrace();
        }

    }
}

4.2.6 GC垃圾回收算法和垃圾收集器的关系?分别是什么请谈谈

  • GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。

  • 4种主要垃圾收集器

    1558949129653.png
    • 串行垃圾收集器(Serial)

      • 它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。
    • 并行垃圾收集器(Parallel)

      • 多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理后台处理等弱交互场景
    • 并发垃圾收集器(CMS)

      • 用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用对响应时间有要求的场景
    • G1垃圾回收器

      • G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
1558948546767.png

4.2.7 怎么查看服务器默认的垃圾收集器是哪个?生产上如何配置垃圾收集器的?谈谈你对垃圾收集器的理解?

  • 怎么查看默认的垃圾收集器是哪个
    • JVM参数:java -XX:+PrintCommandLineFlags -version (-XX:+UseParallelGC)
  • 默认的垃圾收集器有哪些
    • java的gc回收的类型主机有几种:UseSerialGC,UseParallelGC,UseConcMarkSweepGC,UseParNewGC,UseParallelOldGC,UseG1GC
1558951205411.png
1558951448067.png
  • 垃圾收集器
    • 部分参数说明

      • DefNew Default New Generation
      • Tenured Old
      • ParNew Parallel New Generation
      • PSYoungGen Parallel Scavenge
      • ParOldGen Parallel Old Generation
    • Server/Client模式

      • Client模式基本不会用,64位仅支持Swerver模式
    • 新生代

      • 串行GC(Serial)/(Serial Copying)
        • 一句话:一个单线程的收集器,在进行垃圾收集时候,必须暂停其它所有的工作线程直到它收集结束。
        • 串行收集器是最古老,最稳定以及效率最高的收集器,只使用一个线程去回收但是其在回收过程中可能会产生较长时间的停顿(Stop-The-World)状态。虽然在收集垃圾过程中需要暂停所有其它的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是Java虚拟机运行在Client模式下默认的新生代垃圾收集器
        • 对应的JVM参数是:-XX:+UseSerialGC,开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合。表示新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
      • 并行GC(ParNew)
        • 一句话:使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-world暂停其他所有的工作线程直到它收集结束。
        • ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其它的工作线程。它是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器
        • 常见的JVM参数:-XX:+UseParNewGC 启用ParNew收集器,只影响新生代的收集,不影响老年代。开启上述参数后,会使用:ParNew(Young区用)+Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法。
        • 但是,ParNew+Tenured这样的搭配,java8已经不再被推荐, 备注:-XX:ParallelGCThreads限制线程数量,默认开启和CPU核数相同的线程数。
      • 并行回收GC(Parallel)/(Parallel Scavenge)
        • Parallel Scavenge收集器类似ParNew,也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先的收集器。一句话:串行收集器在新生代和老年代的并行化。
        • 它重点关注的是:可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%)。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
        • 自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。
        • 常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器,开启后:新生代使用复制算法,老年代使用标记-整理算法。-XX:ParallelGCThreads=数字N,表示启动多少个GC线程,cpu大于8,N=5/8,cpu小于8,N=实例核数。
    • 老年代

      • 串行GC(Serial Old)/(Serial MSC)
        • Serial Old是Serial垃圾收集器的老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的老年代垃圾收集器。
        • 在Server模式下,主要有2个用途:1、在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。(Parallel Scavenge+Serial Old),2、作为老年代版中使用CMS收集器的后备垃圾收集方案。
      • 并行GC(Parallel Old)/(Parallel MSC)
        • Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。
        • Parallel Old正是为了年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。在JDK1.8及后(Parallel Scavenge+Parallel Old)
        • 常用JVM参数:-XX:+UseParallelOldGC使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代Parallel Old
      • 并发标记清除GC(CMS)
        • CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。 适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短
        • CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
        • Concurrent Mark Sweep:并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行
        • 开启该垃圾收集器的JVM参数:-XX:+UseConcMarkSweepGC 开启该参数后会自动将-XX:+UseParNewGC打开,开启该参数后,使用ParNew(Young区用)+CMS(Old区用)+Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
        • 优点:并发收集低停顿。缺点:并发执行,对CPU资源压力大,由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,会触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。采用的标记清除算法会导致大量内存碎片,无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数:-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
      • 初始标记(CMS initial mark)
        • 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
      • 并发标记(CMS concurrent mark)和用户线程一起
        • 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。
      • 重新标记(CMS remark)
        • 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。
      • 并发清除(CMS concurrent sweep)和用户线程一起
        • 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
    • 垃圾收集器配置总结

      • 底层代码
 // Check consistency of GC selection
/**
 * 检查GC策略的配置是否正确
 */
bool Arguments::check_gc_consistency() {
  check_gclog_consistency();
 
  bool status = true;
  // Ensure that the user has not selected conflicting sets
  // of collectors. [Note: this check is merely a user convenience;
  // collectors over-ride each other so that only a non-conflicting
  // set is selected; however what the user gets is not what they
  // may have expected from the combination they asked for. It's
  // better to reduce user confusion by not allowing them to
  // select conflicting combinations.
  uint i = 0;
 
  if (UseSerialGC)                       i++;
  if (UseConcMarkSweepGC || UseParNewGC) i++;
  if (UseParallelGC || UseParallelOldGC) i++;
  if (UseG1GC)                           i++;
 
  if (i > 1) {
    jio_fprintf(defaultStream::error_stream(),
                "Conflicting collector combinations in option list; "
                "please refer to the release notes for the combinations "
                "allowed\n");
    status = false;
  }
 
  return status;
}
  • 实际代码
1、(DefNew+Tenured)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
2、(ParNew+Tenured)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
3、(PSYongGen+ParOldGen)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
4、(PSYongGen+ParOldGen)(java8默认配置)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC

5、(par new generation+concurrent mark sweep)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
6、(G1)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
7、(java8已经被优化掉了)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC
1558959826312.png
  • 如何选择垃圾收集器
    • 单CPU或小内存,单机程序 -XX:+UseSerialGC
    • 多CPU,需要最大吞吐量,如后台计算型应用 -XX:+UseParllelGC 或者 -XX:+UseParllelOldGC
    • 多CPU,追求低停顿时间,需快速响应如互联网应用 -XX:+UseConcMarkSweepGC -XX:+ParNewGC
1559007533195.png

4.2.8 G1垃圾收集器

  • 以前收集器的特点

    • 年轻代和老年代是各自独立且连续的内存块
    • 年轻代收集使用单eden+s0+s1进行复制算法
    • 老年代收集必须扫描整个老年代区域
    • 都是以尽可能少而快速地执行GC为设计原则
  • G1是什么

    • G1(Garbage-First)收集器,是一款面向服务端应用的收集器。从官网描述中可知道,G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。另外,它还具有以下特性:

    • 像CMS收集器一样,能与应用程序线程并发执行。

    • 整理空闲空间更快。

    • 需要更多的时间来预测GC停顿时间。

    • 不希望牺牲大量的吞吐性能。

    • 不需要更大的Java Heap。

    • G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

      • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
      • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
    • CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题,于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器。

    • G1是在2012年才在JDK1.7u4中可用。oracle官方计划在java9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS收集器。

    • 主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

  • G1特点

    • G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
    • G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。
    • 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘。
    • G1收集器里面将整个的内存区域都混合在一起了,但是其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但是它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然地采用不同的GC方式来处理不同的区域。
    • G1虽然也是分代收集器,但是整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。
  • 底层原理

    • Region区域化垃圾收集器
      • 最大的好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。
      • 区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。
      • 核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连接即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1mb~32mb,且必须是2的幂),默认将整堆划分为2048个分区。
      • 大小范围为1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048=65536MB=64G内存
1559010248200.png

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器,这些Region的一部分包含新生代,新生代 的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。

这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。

在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域,如果一个对象占用的空间超过了分区容量50%以上,G1收集器认为这是一个巨型的对象。这些巨型对象默认直接被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

  • 回收步骤
    • G1收集器下的Young GC
    • 针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片。
      • Eden区的数据移动到Surivor区,假如出现Surivor区空间不够,Eden区数据会部分晋升到Old区
      • Surivor区的数据移动到新的Surivor区,部分数据晋升到Old区
      • 最后Eden区收拾干净了,GC结束,用户的应用程序继续工作。
  • 4步过程
1559791677203.png
  • 常用配置参数
    • -XX:+UseG1GC
    • -XX:G1HeapRegionSize=n: 设置G1区域的大小。但是2的幂,范围1mb-32mb。目标是根据最小的java堆大小划分出约2048个区域。
    • -XX:MaxGCPauseMilis=n:最大GC停顿时间,这个软目标,JVM尽可能停顿少于这个时间
    • -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45
    • -XX:ConcGCThreads=n:并发GC使用的线程数
    • -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
    • 三步归纳:开始G1+设置最大内存+设置最大停顿时间 -XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=100
  • 和CMS相比的优势
    • G1不会产生内存碎片。
    • 是可以精确控制停顿。该收集器把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

4.2.9 生产环境服务器变慢,诊断思路和性能评估谈谈?

  • 整机:top

    • uptime,系统性能命令的精简版
  • CPU:vmstat

    • 查看CPU(包含但不限于)vmstat -n 2 3
      • 一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔单位是秒,第二个参数是采样的次数
      • procs r:运行和等待CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不能超过总核数的2倍,否则代表系统压力过大。b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等。
      • cpu us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序;sy:内核进程消耗的CPU时间百分比。us+sy参考值为80%,如果大于80%,说明可能存在CPU不足。
      • id:处于空闲的CPU百分比。
      • wa:系统等待IO的CPU时间百分比。
      • st:来自于一个虚拟机偷取的CPU时间的百分比。
    • 查看额外
      • 查看所有CPU核信息 mpstat -P ALL 2
      • 每个进程使用cpu的用量分解信息 pidstat -u 1 -p 进程编号
[root@vm10.10.10.10 ~]# vmstat -n 2 3
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  0      0 458120 300268 1545292    0    0     1    11    2    2  1  0 99  0  0   
 0  0      0 458988 300268 1545336    0    0     0    16  756 2123  2  1 97  0  0   
 0  0      0 459112 300268 1545416    0    0     0    40 1035 2398  2  1 97  0  0   
  • 内存:free -m
    • 应用程序可用内存数
      • 应用程序可用内存/系统物理内存>70%,内存不足
      • 应用程序可用内存/系统物理内存<20%,内存不足,需要增加内存
      • 20%<应用程序可用内存/系统物理内存<70%内存基本够用。
    • 查看额外 pidstat -p 进程号 -r 采样间隔秒数
  • 硬盘:df
    • 查看磁盘剩余空间数 df -h
  • 磁盘IO: iostat
    • 磁盘I/O性能评估 (iostat -xdk 2 3)
      • 磁盘块设备分布
      • rkB/s每秒读取数据量kB;
      • wkB/s每秒写入数据量kB;
      • svctm I/O请求的平均服务时间,单位毫秒;
      • await I/O请求的平均等待时间,单位毫秒;值越少,性能越好;
      • util 一秒中有百分之几时间用于I/O操作。接近100%时,表示磁盘带跑满,需要优化程序或者增加磁盘
      • rkB/s、wkB/s根据系统应用不同会有不同的值,但有规律遵循:长期、超大内存读写,肯定不正常,需要优化程序读取。
      • svctm的值与await的值很接近,表示几乎没有I/O等待,磁盘性能好,如果await的值远高于svctm的值,则表示I/O队列等待太长,需要优化程序或更换快磁盘。
    • 查看额外 pidstat -d 采样间隔秒数 -p 进程号
  • 网络IO:ifstat
    • 默认本地没有,下载安装ifstat ./configure make && make install
    • 查看网络IO (ifstat -1) ,各个网卡的in、out,观察网络负载情况,程序网络读写是否正常,程序网络I/O优化,增加网络I/O带宽。

4.2.10 假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位

  • 结合linux命令的JDK命令一块分析
  • 先用top命令找出CPU占比最高的pid
  • ps -ef或者jps进一步定位, 得知是一个什么后台程序(ps -ef|grep pid|grep -v grep)
  • 定位到具体线程或者代码
    • ps -mp 进程 -o THREAD,tid,time
    • -m 显示所有的线程, -p pid进程使用CPU的时间,-o 该参数后是用户自定义格式
  • 将需要的线程ID转换为16进制格式(英文小写格式)
    • print "%x\n" 有问题的线程ID
  • jstack 进程ID|grep tid(16进制线程ID小写英文) -A60

4.2.11 对于JDK自带的JVM监控和性能分析工作用过哪些?一般你是怎么用的?

  • jps(虚拟机进程状况工具,列出当前机器上正在运行的虚拟机进程)
    • -p :仅仅显示VM 标示,不显示jar,class, main参数等信息.
    • -m:输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数
    • -l: 输出应用程序主类完整package名称或jar完整名称.
    • -v: 列出jvm参数, -Xms20m -Xmx50m是启动程序指定的jvm参数
  • jinfo(java配置信息工具,查看和修改虚拟机的参数)
    • jinfo –sysprops 可以查看由System.getProperties()取得的参数
    • jinfo –flag 未被显式指定的参数的系统默认值
    • jinfo –flags(注意s)显示虚拟机的参数
    • jinfo –flag +[参数] 可以增加参数,但是仅限于由java -XX:+PrintFlagsFinal –version查询出来且
      为manageable的参数
    • jinfo –flag -[参数] 可以去除参数
  • jmap(内存映像工具)
    • 官网
    • 用于生成堆转储快照(一般称为heapdump或dump文件)。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。
    • 和jinfo命令一样,jmap有不少功能在Windows平台下都是受限的,除了生成dump文件的-dump选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris下使用。
    • jmap -dump:live,format=b,file=heap.bin <pid>
      Sun JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。
    • jhat dump文件名
      后屏幕显示“Server is ready.”的提示后,用户在浏览器中键入http://localhost:7000/就可以访问详情
    • case
      • 映射堆快照 jmap -heap 进程ID
      • 抓取堆内存
        • 生成hprof文件并下载到本地
        • MAT分析插件工具
  • jstat(统计信息监视工具)
    • 官网
    • 解释
      • jstat命令是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
      • jstat [-命令选项][vm进程ID] 间隔时间(单位毫秒)查询次数,假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:jstat-gc 2764 250 20
      • 常用参数:
        • -class (类加载器)
        • -compiler (JIT)
        • -gc (GC堆状态)
        • -gccapacity (各区大小)
        • -gccause (最近一次GC统计和原因)
        • -gcnew (新区统计)
        • -gcnewcapacity (新区大小)
        • -gcold (老区统计)
        • -gcoldcapacity (老区大小)
        • -gcpermcapacity (永久区大小)
        • -gcutil (GC统计汇总)
        • -printcompilation (HotSpot编译统计)
    • case
      • 类加载统计
      • 编译统计
      • 垃圾回收统计(jstat -参数 线程id 执行时间(单位毫秒) 执行次数)
  • jstack(堆栈异常跟踪工具)
    • 命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。
    • 在代码中可以用java.lang.Thread类的getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
    • 管理远程进程需要在远程程序的启动参数中增加:
      -Djava.rmi.server.hostname=…..
      -Dcom.sun.management.jmxremote
      -Dcom.sun.management.jmxremote.port=8888
      -Dcom.sun.management.jmxremote.authenticate=false
      -Dcom.sun.management.jmxremote.ssl=false
  • jvisualvm
  • jconsole

4.3 其他

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

推荐阅读更多精彩内容