一、简单了解几个概念

1、什么是垃圾(Garbage)?什么是垃圾回收(Garbage Collection,简称 GC)?

(1)什么是垃圾(Garbage)?
  这里的垃圾 指的是 在程序运行过程中没有任何指针指向的对象,即不再被使用的对象。
  如果不及时清理这些对象(垃圾),这些对象将会占用程序内存,无法被其他对象使用,严重时可能导致内存溢出。

(2)什么是垃圾回收(Garbage Collection,简称 GC)?
  一般程序占用的内存都是有限的,如果不断分配内存空间而不进行内存回收,内存迟早会被消耗完。所以要对 这些没用的垃圾对象进行内存空间的回收(即 GC),释放没用的对象、整理内存碎片,使整理出来的内存可以分配给新对象使用。
  随着程序业务的庞大、复杂,GC 操作尤为重要,没有 GC 即意味着系统不能正常运行,而经常导致 STW(Stop The World,GC 回收导致程序暂停直至 GC 结束) 的 GC 也逐渐跟不上实际需求,所以后续出现各式各样的 GC 优化后的 GC 收集器(比如:CMS、G1 等)。

2、内存自动管理?

(1)C/C++ 手动管理内存
  在早期的 C/C++ 时代,都是手动进行 内存分配 以及 内存回收(GC)。这种方式可以灵活控制内存释放时间,但是开发人员需要 频繁操作 内存申请以及内存释放,此时若开发人员疏忽而漏掉了某处的内存回收,可能会造成 内存泄露。由于此时垃圾对象无法被清除,随着系统运行可能会持续消耗内存,最终导致内存溢出使程序崩溃。

(2)Java 自动管理内存
  Java 使用 内存自动管理思想,其 内存动态分配 以及 内存自动回收 机制 使开发人员减轻了 内存操作的压力(无需手动参与内存分配与回收,可以专注于业务开发),降低了 内存泄露 与 内存溢出的 风险。但是 过于依赖内存自动管理,将会弱化 开发人员 定位、解决 程序中 内存溢出、内存泄露 等问题的能力。
  所以了解 JVM 自动内存分配 以及 内存回收 等原理是非常重要的,便于排查各种 内存溢出、内存泄露 等问题,当系统因 GC 出现瓶颈,可以对其进行适当的监控与调优。

3、简单了解下 内存泄露、内存溢出(OOM)

(1)内存泄露(Memory Leak):
  内存泄露 指的是程序在申请内存运行后,无法释放已经申请的内存空间,从而导致程序运行速度变慢甚至崩溃。
  简单的理解就是,你开辟了一个空间使用,使用完之后却不释放该空间,导致该空间一直被占用(内存泄露次数过多,占用内存也就更多,此时可能导致内存溢出)。

导致内存泄露出现的情况一般为:对象生命周期过长,无法被垃圾回收器收集。
  对象生命周期过长:
    比如单例模式产生的对象,生命周期与应用程序一样长,如果该对象内部持有 外部对象的引用,那么这个外部对象是不会被垃圾收集器回收的,从而导致内存泄露。
    一些资源未关闭,比如 数据库连接、IO 连接 未关闭,是不会被回收的,从而导致内存泄露。

(2)内存溢出(Out Of Memory):
  内存溢出 指的是程序运行时 申请内存 大于 系统剩余内存空间,导致内存分配失败使系统崩溃。
  简单的理解就是,你现在需要开辟 10M 的内存空间,但是系统只剩余 9M 内存空间,最终系统无法分配所需的内存导致内存分配失败。

  一般情况下,除非应用程序占用内存增长速度非常快,造成垃圾回收速度跟不上内存消耗的速度,否则不太容易出现 内存溢出(OOM)的情况。
导致 OOM 出现情况一般为:空闲内存不足 且 垃圾回收器不能提供更多的内存。

  空闲内存不足:
    Java 虚拟机设置的 堆内存 不够。可通过 -Xms、-Xmx 参数调整。
    代码中创建了大量大对象且长时间不能被垃圾回收器收集。

4、垃圾回收的目标区域

(1)垃圾回收区域:
  JVM 运行时数据区 包括 程序计数器、虚拟机栈、本地方法栈、堆、方法区。
其中:
  程序计数器、虚拟机栈、本地方法栈 随着线程产生、结束,而栈的栈帧 也是随着方法进入、退出而执行入栈、出栈操作,即 方法结束或者线程结束,其内存就可以跟着回收,所以这些可以不需要过多考虑 内存回收问题。
  而 堆、方法区 需要运行时才能确定内存大小,比如 运行时才可以确定 会创建哪些对象、对象需要消耗多少空间等。这样的区域 内存分配 与 回收是动态的,也即垃圾回收的重点关注对象,其中,堆 是重点中的重点。

(2)垃圾回收目标:
  GC 根据不同区域又可划分为:年轻代回收、老年代回收、全堆回收、方法区回收。
  但从回收频率上:频繁收集年轻代、较少收集老年代、基本不动方法区。

5、理解一下 System.gc()

(1)System.gc() 作用
  默认情况下,调用 Runtime.getRuntime().gc() 或者 System.gc() 会显示触发 Full GC,同时对老年代、新生代进行垃圾回收,并尝试释放被丢弃对象占用的内存。
  但是 System.gc() 不能保证立即进行垃圾回收,甚至不一定会执行垃圾回收。垃圾回收一般是自动进行的,不推荐手动触发(会导致 STW)。

(2)System.gc() 回收举例

【举例:】
public class JVMDemo {

    public static void main(String[] args) {
        JVMDemo jvmDemo = new JVMDemo();
//        jvmDemo.testGC1();
//        jvmDemo.testGC2();
//        jvmDemo.testGC3();
//        jvmDemo.testGC4();
        jvmDemo.testGC5();
    }

    public void testGC1() {
        // GC 不回收 有用的对象
        byte[] buffer = new byte[20 * 1024 * 1024];
        System.gc();
    }

    public void testGC2() {
        // GC 回收 无用的对象,此时对象置 null,即引用失效,为垃圾对象
        byte[] buffer = new byte[20 * 1024 * 1024];
        buffer = null;
        System.gc();
    }

    public void testGC3() {
        // 引用仍存在栈帧的 局部变量表中,不会被 GC 回收
        {
            byte[] buffer = new byte[20 * 1024 * 1024];
        }
        System.gc();
    }

    public void testGC4() {
        // 新的局部变量占用 过期的局部变量 在局部变量表的位置,即引用失效,可以被 GC 回收
        {
            byte[] buffer = new byte[20 * 1024 * 1024];
        }
        int value = 10;
        System.gc();
    }

    public void testGC5() {
        // 方法作用域结束,引用失效,可被 GC 回收
        testGC1();
        System.gc();
    }
}

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

6、Stop The World(STW)、并行(Parallel)、并发(Concurrent)

(1)Stop The World
  简称 STW,指的是 GC 发生时程序会停顿(停顿产生使整个应用程序线程都被暂停,没有任何响应),在 GC 完成后,应用程序将被恢复。要减少 STW 的发生。
  一般来说 任何 GC 均会产生 STW,只能说 GC 回收器越来越优秀,回收效率越来越高,从而导致 STW 时间缩短(体会不到停顿的发生)。

(2)并行(Parallel)
  当系统有多个 CPU 时(或者 多核 CPU 时),某个 CPU 执行一个进程时,其他 CPU 可以执行其他的进程,各 CPU 之前互不抢占 CPU 资源,即真正的同时执行。

(3)并发(Concurrent)
  在操作系统中,某一时间段上 同一个处理器 运行多个线程。通过 CPU 调度算法,使各个线程在时间段内快速切换,使程序看上去是同时执行的,不是真正的同时执行。

(4)并行、并发区别
  并发:强调多个事情 在同一时间段内 同时发生了。多个任务之间相互抢占资源。
  并行:强调多个事情 在同一时间点上 同时发生了。多个任务之间不相互抢占资源。

(5)垃圾回收中的并行与并发
  串行:指单个垃圾回收线程执行,此时用户线程暂停。
  并行:指多个垃圾回收线程同时执行,此时用户线程暂停。
  并发:指用户线程、垃圾回收线程交替执行,用户线程不暂停。

7、安全点(SafePoint)、安全区域(SafeRegion)

(1)安全点:
  程序执行时并非可以在任意地方停顿并 GC,强制要求在特定位置才能停顿并 GC,而这些位置称为 安全点。
  安全点设置太少 可能导致 GC 等待时间长,设置过多 可能导致 运行时性能下降。
  大部分指令执行时间非常短暂,但也有一部分指令执行时间会较长,为了避免程序长时间无法进入安全点导致 GC 等待时间长,所以一般安全点选择标准为:是否具有使程序长时间执行能力的指令作为安全点,比如:方法调用之后、循环末尾、方法返回前等。

(2)如何使线程在安全点完成停顿?
抢占式中断(一般 JVM 都不采用):
  先中断所有线程,如果有线程不在安全点,则恢复该线程,过一会再中断直至线程达到安全点。

主动式中断(一般 JVM 采用):
  设置一个中断标志位,各个线程运行到安全点 时主动轮询该标志,如果中断标志为真,则线程挂起。

(3)安全区域:
  安全点是针对 正在执行的线程 设定的,如果线程处于 sleep 或者 block 等中断状态,其不能响应 JVM 的中断请求、运行到 安全点 进行中断。为了解决这个问题,产生了安全区域。
  安全区域指的是一段代码片段中,对象的引用关系不会发生变化,此时这个区域中任何位置开始的 GC 均是安全的。

(4)安全区域执行:
  当线程运行到 安全区域时,先标记自己进入 安全区域,如果这段时间内发生 GC,则 JVM 会忽略被标识为 安全区域 状态的线程。
  当线程即将离开 安全区域 时,先检查是否完成 GC,如果完成 GC 则继续运行,否则线程必须等待、直到收到可以离开 安全区域 的信号。

8、强引用、软引用、弱引用、虚引用

(1)引用
  JDK1.2 后,Java 对引用概念进行了补充,将引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference) 四种,且引用强度依次减弱。

  垃圾收集器回收对象时,回收目标一般为 未被引用的对象。若想回收一些被引用的对象(比如类似于缓存的存在,当内存空间足够时,保留对象,若垃圾回收后内存空间依旧不够,则回收对象增大内存空间),可以使用弱引用、软引用等去实现。

(2)Reference
  java.lang.ref 包下定义了 Reference 抽象类,其有不同的子类可以实现不同的引用效果,其中除 FinalReference (default,包内可见)外,其余三种引用类型均为 public,可以直接使用。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(3)强引用 -- 不回收(可能导致 OOM)
  最基本的引用,最常见的引用赋值操作。强引用一般指通过 new 关键字创建对象并赋值给变量,此时变量成为指向对象的强引用。比如: Object object = new Object(); 。
  只要强引用存在,垃圾回收器不会回收被引用的对象。
注:
  强引用是造成 内存泄露、内存溢出 的主要原因之一。
  若想回收一个强引用对象,可以显示将其强引用赋值为 null,或者超出强引用的作用域。

【举例:(强引用存在就不会回收内存)】
public class JVMDemo {

    public static void main(String[] args) {
        // 申请 10M 内存空间
        byte[] buffer = new byte[10 * 1024 * 1024];
        // 第一次强引用存在,不会回收
        System.gc();

        // 消除强引用
        buffer = null;
        // 第二次强引用不存在,回收内存空间
        System.gc();
    }
}

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(4)软引用 -- 内存不足就回收
  软引用 一般用来 描述 还有用但是非必须的 对象。
  在系统发生 OOM 之前,会对软引用对象进行 二次回收,若此次回收仍没有足够内存,才会抛出 OOM 异常。
  软引用类似于缓存的存在,内存不足时才会去清理。

【举例:(当内存不足时,触发垃圾回收,并回收软引用所占内存)】
import java.lang.ref.SoftReference;

/**
 * JVM 参数设置 -XX:+PrintGCDetails -Xmx15m -Xms15m
 */
public class JVMDemo {

    public static void main(String[] args) {
        /**
         *  Test test = new Test();
         *  SoftReference<Test> softReference = new SoftReference<>(test);
         *  test = null;
         *
         *   上面三行代码等价于下面一行代码
         *  SoftReference<Test> softReference = new SoftReference<>(new Test());
         */
        SoftReference<byte[]> softReference = new SoftReference<>(new byte[10 * 1024 * 1024]);
        // 第一次内存足够,即使手动 GC,软引用也不会被回收
        System.out.println(softReference.get());
        System.gc();
        System.out.println(softReference.get());

        // 第二次内存不够,触发 GC,软引用被回收
        byte[] buffer = new byte[8 * 1024 * 1024];
        System.out.println(softReference.get());
        System.gc();
        System.out.println(softReference.get());

    }
}

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(5)弱引用 -- 被发现就回收
  弱引用也是用来描述非必须对象,其强度弱于 软引用,被弱引用关联的对象只能生存到下一次垃圾收集前。当系统 GC 时,只要发现了弱引用,无论堆空间内存是否足够,都会回收掉弱引用关联的对象。
  弱引用同样可以用于实现缓存。

【举例:(弱引用被发现就回收)】
import java.lang.ref.WeakReference;

/**
 * JVM 参数设置 -XX:+PrintGCDetails -Xmx15m -Xms15m
 */
public class JVMDemo {

    public static void main(String[] args) {
        byte[] buffer = new byte[10 * 1024 * 1024];
        WeakReference<byte[]> weakReference = new WeakReference<>(buffer);
        System.out.println(weakReference.get());
        System.gc();
        System.out.println(weakReference.get());

        buffer = null; // 去除强引用,此时弱引用可被回收
        System.out.println(weakReference.get());
        System.gc();
        System.out.println(weakReference.get());
    }
}

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

注:
  WeakHashMap、ThreadLocal 中都使用到了 弱引用。
  WeakHashMap 是基于弱引用实现的,其存储的对象可能随时被回收,即 使用 WeakHashMap 存储元素时,即使你没有进行删除元素操作,其最后保存的值也可能不一样。可用于实现缓存。

【举例:】
import java.util.WeakHashMap;

/**
 * JVM 参数设置 -XX:+PrintGCDetails -Xmx15m -Xms15m
 */
public class JVMDemo {

    public static void main(String[] args) {
        for (int j = 0; j < 3; j++) {
            WeakHashMap<Integer, Object> weakHashMap = new WeakHashMap<>();
            for (int i = 0; i < 1000; i++) {
                weakHashMap.put(i, new Object());
            }
            System.out.println("未进行 GC 前:     " + weakHashMap.size());
            System.gc();
            System.out.println("进行 GC 后:       " + weakHashMap.size());
        }
    }
}

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(6)虚引用 -- 用于对象回收跟踪
  虚引用是引用类型中最弱的一个,一个对象是否有虚引用的存在,不会影响对象的生命周期,其不能单独使用、也无法通过虚引用获取到被引用的对象(调用 get 方法返回 null),其唯一作用就是跟踪垃圾回收过程(对象被回收时收到一个系统通知)。
  虚引用需要与引用队列一起使用(创建虚引用时提供一个引用队列对象作为参数),当 GC 准备回收一个对象时,若发现其有虚引用,在回收对象后会将这个虚引用加入引用队列,可以通过 引用队列是否存在虚引用来了解 对象是否被垃圾回收 并作出相应处理。

【举例:(引用对象加入 引用队列,通过引用队列是否存在虚引用 作出相应处理)】
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

/**
 * JVM 参数设置 -XX:+PrintGCDetails -Xmx15m -Xms15m
 */
public class JVMDemo {

    public static void main(String[] args) {
        byte[] buffer = new byte[10 * 1024 * 1024];
        ReferenceQueue referenceQueue = new ReferenceQueue<>();
        PhantomReference<byte[]> phantomReference = new PhantomReference<>(buffer, referenceQueue);
        System.out.println(phantomReference.get() + "==========" + referenceQueue.poll() + "=========" + phantomReference);

        // 消除强引用,GC 回收后,虚引用会进入 引用队列,此时虚引用不会置 null
        buffer = null;
        System.gc();
        System.out.println(phantomReference.get() + "==========" + referenceQueue.poll() + "=========" + phantomReference);

        // 手动释放 虚引用对象堆空间
        phantomReference.clear();
        System.gc();
        System.out.println(phantomReference.get() + "==========" + referenceQueue.poll() + "=========" + phantomReference);
    }
}

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

二、垃圾标记算法

1、标记算法有什么用?

(1)标记算法作用?
  堆中几乎存储了 Java 程序中的实例对象,如何确定哪些 对象 属于无用对象 是 GC 的关键。
  标记算法作用就是标记出哪些对象无用(当一个对象 不被 任何对象引用时,可以将其视为 死亡对象,即垃圾对象),从而方便 GC 进行内存回收。

(2)常用标记算法分类:
  常用标记算法有 引用计数算法 和 可达性分析算法。
  JVM 一般采用 可达性分析算法。

2、标记算法 -- 引用计数算法(Reference Counting)

(1)引用计数算法(Reference Counting):
  引用计数算法为 每个对象维护了一个 引用计数器属性,用于记录对象被引用的情况。
  简单的理解:为每个对象添加一个 引用计数器,当对象被引用时,引用计数器加 1,当引用失效时,引用计数器减 1。当引用计数器为 0 时,表示对象无用,可被回收。

(2)优缺点:
优点:
  实现简单,垃圾对象标识清晰,回收效率高。

缺点:
  需要内存空间维护 引用计数器,增加了内存空间的开销。
  每次赋值操作会触发 引用计数器 的加减法,增加了时间的开销。
  无法处理 循环引用 问题,导致一般 JVM 并没有采用这个算法进行垃圾回收。
注:
  虽然 Java 并未选择 引用计数算法,但是仍有其他语言选择,比如 Python。Python 解决循环引用的方式:手动解除(在合适的场合,主动解除引用关系) 或者 使用弱引用 weakref(weakref 是 Python 提供的标准库,用于解决循环引用)。

(3)引用计数算法无法解决循环引用 举例:
  如下,实例化两个对象,此时引用计数器为 1,然后使两个对象 相互引用,此时引用计数器为 2,然后将对象置 null,即引用计数器减 1,此时理论上说,这两个对象都已失效,但是由于两个对象相互引用导致 引用计数器不为 0,从而无法进行回收(造成内存泄露)。

【举例:】
public class JVMDemo {

    public static void main(String[] args) {
        CircularReference a = new CircularReference();
        CircularReference b = new CircularReference();
        a.ref = b;
        b.ref = a;
        a = null;
        b = null;
        System.gc();
    }
}

class CircularReference {
    CircularReference ref = null;
    private byte[] size = new byte[3 * 1024 * 1024];
}

3、标记算法 -- 可达性分析算法(Reachability Analysis)

(1)可达性分析算法(Reachability Analysis)
  可达性分析 又可称为 根搜索算法 或者 追踪性垃圾收集。
  其以 根对象集合(GC Roots)为起始点,根据引用关系 从上至下 搜索,搜索过程中走过的路径称为 引用链(Reference Chain),所有不在 引用链 上的对象 均为不可达对象(即无用对象,可标记为垃圾)。
注:
  在可达性算法中,只有能够被 GC Roots 直接或间接连接的对象才属于 有用对象。
  GC Roots 是一系列符合条件的对象节点(一组活跃引用的集合),并非某一个节点。
  可达性分析算法解决了 循环引用问题,一般 JVM 均采用此算法标记可回收对象。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(2)GC Roots 分类
GC Roots 是一组活跃引用的集合。包括如下几类:
  虚拟机栈中 引用的对象。比如:各个线程被调用的方法中使用的 参数、局部变量等。
  本地方法栈中 引用的对象。比如:native 方法引用的对象。
  方法区中 类静态属性 引用的对象。比如:Java 类的引用类型的静态变量。
  方法区中 常量 引用的对象。比如:运行时常量池中的引用。
  所有被同步锁(synchronized 关键字)持有的对象。
  JVM 内部的 引用对象。比如:基本类型对应的 Class 对象、系统类加载器、OOM 异常对象等。

除了以上分类,根据不同的垃圾收集器 以及 内存回收区域的不同,会产生一些 临时对象 进入 GC Roots 中。比如:分代收集、局部回收(Partial GC)。

一般来说,一个引用指向了堆内存中的对象,但是其本身并不存放在堆内存中,那么其就属于 Root。

注:
  使用可达性分析算法标记对象时,必须保证分析工作 在能保障一致性的快照中进行。简单的理解就是,分析对象是否可以回收时,不能操作对象,否则可能导致分析结果不正确。

4、对象的 finalization 机制、 finalize() 方法

(1)对象的 finalization 机制
  Java 提供的对象终止机制(finalization)可以允许 开发人员 在对象被销毁前进行自定义逻辑处理。
  当垃圾回收器发现 无用对象(没有引用指向对象)时,会先去调用该对象的 finalize() 方法,该方法属于 Object 类,可以被子类重写,用于对象被回收时进行资源的释放(比如关闭文件、关闭数据库连接等)。

(2)finalize() 方法
  一般不建议主动调用对象的 finalize() 方法,应该交给垃圾回收机制 触发(该方法只会被触发一次)。
理由:
  使用 finalize() 方法可能导致对象复活(后面介绍,此处有个印象)。
  finalize() 方法执行时间没有保障,极端情况下若不发生 GC,则不会去执行该方法。
  finalize() 方法执行可能会影响 GC 性能(比如代码里发生了死循环)。

(3)对象的状态
  执行可达性分析算法后,从根节点无法访问到的对象,一般都是需要被回收的,但事实上也许这些对象不一定 非死不可。比如 某个对象可能会在 某个条件下 复活自己,此时对该对象的回收就是不合理的。

一般对象状态分类如下:
  可达对象,即 从根节点开始可以访问的对象。
  可复活对象,即 对象引用已被释放,GC 回收触发 finalize() 方法时对象被复活。
  不可达对象,即 对象引用已被释放,GC 回收触发 finalize() 方法时没有复活对象。
注:
  对象为 不可达状态时 才会被回收。
  GC 只会触发一次对象的 finalize() 方法,对象复活后,下一次 GC 并不会触发该方法。

(4)两次标记对象的流程:
Step1:如果对象 没有被 GC Roots 引用链关联,则进行第一次标记。
Step2:筛选对象 是否需要执行 finalize() 方法。
  Step2-1:如果对象 没有重写 finalize() 方法 或者 finalize() 方法已被调用过,则对象直接判定为 不可达对象,可以被回收。
  Step2-2:如果对象 重写了 finalize() 方法且未被执行,则对象会被插入到 F-Queue 队列中,并由 JVM 自动创建的低优先级的 Finalizer 线程触发其 finalize() 方法执行。
  Step2-3:GC 会对 F-Queue 队列中的对象进行 二次标记,将对象放入 “即将回收” 的集合,如果 finalize() 方法执行过程中 对象与引用链上的对象建立关联(可以将 this 关键字赋值给某个类变量 或者 对象的成员变量),即此时对象已复活,当该对象再次与 引用链无关联时,会直接变为不可达对象。

【举例:】
public class JVMDemo {
    // 定义一个类变量,作为 GC Roots
    public static JVMDemo jvmDemo = null;

    public static void main(String[] args) throws InterruptedException {
        // 实例化
        jvmDemo = new JVMDemo();

        // 第一次触发 GC,会触发 finalize() 方法,复活对象
        jvmDemo = null;
        System.gc();
        // Finalizer 线程优先级较低,暂停一下让它有时间响应
        Thread.sleep(500);
        if (jvmDemo != null) {
            System.out.println("我还活着");
        } else {
            System.out.println("我死了");
        }

        // 第二次触发 GC,不会触发 finalize() 方法,可直接回收对象
        jvmDemo = null;
        System.gc();
        // Finalizer 线程优先级较低,暂停一下让它有时间响应
        Thread.sleep(500);
        if (jvmDemo != null) {
            System.out.println("我还活着");
        } else {
            System.out.println("我死了");
        }

    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("执行 finalize() 方法");
        // this 赋值给 类变量,与 引用链建立联系,即复活对象
        JVMDemo.jvmDemo = this;
    }
}

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

三、垃圾清除算法

1、什么是垃圾清除?

(1)什么是垃圾清除?
  通过前面的垃圾标记 算法,可以在内存中区分 存活对象 以及 死亡对象(无用对象),接下来 GC 需要执行垃圾清除,释放掉无用对象所占用的内存空间,以便于有足够可用的内存空间存放新对象。

(2)常见垃圾清除算法:
  标记-清除算法(Mark-Sweep)。
  标记-复制算法(Mark-Copying)。
  标记-压缩算法(Mark-Compact)。

2、清除算法 -- 标记-清除算法(Mark-Sweep)

(1)什么是标记-清除算法?
  是最早出现、最基础的垃圾收集算法,其分为 “标记”、“清除” 两个阶段。
  当堆中有效内存空间被消耗完时,会暂停整个程序(Stop The World,简称 STW),并开始执行垃圾回收(标记 与 清除)。
    标记:从 GC Roots 开始遍历,标记所有由 GC Roots 直接或间接关联的对象,未标记的对象均为 垃圾对象,可被回收。
    清除:从堆内存中开始遍历,如果发现未标记的对象(即无用对象),则将其回收。

如下图所示(图片来源于网络):

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

 学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(2)缺点:
  进行 GC 时,会暂停整个应用程序,导致用户体验差。
  当堆中包含大量需要被回收对象时,有大量标记、清除动作,执行效率低。
  回收空间不连续,存在大量内存碎片,需要维护一个空闲列表来管理内存分配,当分配大对象且没有足够的连续空间时,可能会提前触发一次 GC。

注:
  清除并非为置空,而是将需要清除的对象地址保存在空闲列表中,下次分配新对象时,如果空间足够就将其覆盖。

3、清除算法 -- 标记-复制算法(Mark-Copying)

(1)什么是标记-复制算法?
  为了解决 标记-清除 算法 面对大量可回收对象执行效率低的问题,引出了 半区复制 算法。
  其将内存空间 分为两块,每次只使用其中一块,当某一块内存被使用完,就将此内存中仍然存活的对象 复制 到未使用 的内存块中,并清除当前正在使用的内存块的所有对象,交换两个内存块的角色,最终完成垃圾回收。

注:
  年轻代中, survivor 区就是采用这种形式进行垃圾回收。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

 学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(2)优缺点
优点:
  其保证了内存空间的连续性,不会产生内存碎片(移动指针,按顺序分配内存空间)。

缺点:
  需要两倍内存空间,且会浪费 一块内存空间。
  如果内存中有大量对象存活,那么对象复制 会产生时间开销,且执行效率低。

4、清除算法 -- 标记-压缩(标记-整理) 算法(Mark-Compact)

(1)什么是标记 - 压缩算法?
  复制算法高效性建立在 存活对象少、垃圾对象多 的情况下,比如年轻代。但是老年代,大部分对象都是存活对象,如果仍使用复制算法,那么其对象复制的时间开销将会很高。
  为了解决上面的问题,在 标记-清除 的基础上改进,产生了 标记-压缩算法。分为标记、压缩 两个阶段。先标记出所有存活的对象,然后将这些对象按照顺序压缩内存的一端,最后清理掉边界之外的内存空间。

注:
  标记-压缩算法最终效果 等同于 标记-清除-压缩 的过程,即先执行标记、清除的过程,最后将内存碎片整理(压缩)。
  标记-清除算法 是一种 非移动式的回收算法,标记-压缩算法 是一种 移动式的回收算法,各有优缺点。移动对象时 内存回收会很复杂(对象移动会触发 STW,若对象被其他对象引用,还需修改其引用地址,但是分配内存更容易),不移动对象时,内存分配会很复杂(内存碎片多,需要使用空闲列表维护内存碎片)。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

 学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(2)优缺点
优点:
  解决了 标记-清除 算法中的内存碎片问题,分配内存给新对象时,只需要分配一个内存的起始地址即可。
  解决了 标记-复制 算法中的两倍内存问题。

缺点:
  效率上 标记-压缩 算法 比 复制算法 低。
  移动对象时,需要暂停程序,即 STW。
  移动对象时,若对象被其他对象引用,还需要调整引用地址。

5、清除算法 -- 分代收集算法(Generational Collection)

(1)常见清除算法使用场景
  标记-清除算法 虽然会产生内存碎片,但其不需要移动对象,适合存活对象较多的场景。
  标记-复制算法 虽然会消耗两倍内存空间 且 需要移动对象,但其不会产生内存碎片,执行效率也较高,适合存活对象较少的场景。
  标记-压缩算法 属于 标记-清除算法的优化版,其需要移动对象 但不会产生内存碎片,可用于存活对象较多的场景。

(2)什么是分代收集算法?
  没有更优的算法,只有更合适的算法。上面介绍的几种算法,各有优缺点,可用于不同场景下。为了综合这些算法的优点,分代收集算法出现了。

分代收集算法:
  不同对象的生命周期是不同的,针对不同生命周期的对象采用不同的收集方式,可以提高垃圾回收效率。比如堆可以分为 年轻代、老年代,针对各年代的特点使用不同的回收算法,从而提高垃圾回收的效率。

(3)HotSpot 分代算法
年轻代:
  年轻代占用内存区域较小,对象生命周期较短、存活率低、回收频繁。
  此区域可以采用复制算法进行回收,执行速度快,比如 survivor 区的实现。

老年代:
  老年代占用内存区域较大,对象生命周期较长、存活率高、回收不频繁。
  此区域可以采用 标记-清除 或者 标记-清除、标记-整理 混合回收。比如 CMS 回收器,基于 标记-清除 算法实现,当内存碎片过多 并影响到 对象分配时,采用 标记-压缩 算法进行内存碎片的整理。

6、清除算法 -- 增量收集算法(Incremental Collection)

(1)什么是增量收集算法?
  垃圾回收过程中,程序会处于 STW 的状态,程序线程会被挂起直至垃圾回收结束。如果垃圾回收时间较长,程序将被挂起很久,影响用户体验 以及 系统稳定性。为了解决这个问题,增量收集算法出现了。

增量收集算法:
  如果一次性对所有垃圾进行回收,可能造成系统长时间停顿。可以采用垃圾回收线程 与 应用程序线程 交替执行的形式,每次垃圾收集线程收集一小片区域后,切换到应用程序线程执行,然后又切回垃圾收集线程进行垃圾收集,如此反复直至垃圾回收完毕。

注:
  增量收集算法建立在 标记-清除 以及 复制算法基础上,处理线程间冲突、允许垃圾收集线程 采用分阶段标记 形式完成标记、清理、复制等工作。


(2)优缺点:
优点:
  在垃圾回收过程中,间断性的执行应用程序代码,可以减少系统停顿时间。

缺点:
  线程切换会消耗资源。

7、清除算法 -- 分区算法

(1)什么是分区算法?
  一般情况下,堆空间越大,GC 耗时越长,而 STW 时间也越长。为了更好控制 GC 产生的停顿时间,可以将一个大的内存区域 分隔成 多个小内存区域。每次回收若干个小区间而非整个堆空间,从而减少停顿时间。

(2)分代算法、分区算法区别?
  分代算法 按照对象的生命周期长短将堆划分为 年轻代、老年代 进行垃圾回收。
  分区算法 将堆划分为若干个小区间,每次对部分小区间进行垃圾回收。

四、垃圾收集器

1、垃圾收集器

(1)概述
  JVM 规范中并未对垃圾收集器如何实现做出过多的规定,不同的厂商、不同版本的 JVM 内部使用的垃圾收集器也可能由很大差别。

(2)垃圾收集器分类
按线程数划分:
  串行垃圾回收器。指同一时间段内只允许一个线程执行垃圾回收操作,应用程序线程将被暂停直至垃圾收集结束。适用于并发能力较弱的机器。
  并行垃圾回收器。指允许多线程执行垃圾回收操作,能减低应用程序线程暂停时间。适用于并行能力较强的机器。

按工作模式划分:
  并发式垃圾回收器。指 垃圾回收线程 与 应用程序线程交替工作,降低应用程序暂停时间。
  独占式垃圾回收器。指 垃圾回收线程执行时,应用程序线程将暂停直至垃圾收集结束。

按内存空间划分:
  年轻代垃圾回收器。收集 年轻代 内存空间。
  老年代垃圾回收器。收集 老年代 内存空间。

按内存碎片处理划分:
  压缩式垃圾回收器。指 垃圾回收完成后,对存活对象进行压缩整理,清除内存碎片,再次分配对象内存空间时可以采用 指针碰撞的方式。
  非压缩式垃圾回收器。指 不清理内存碎片,再次分配对象内存空间时可以采用 空闲列表的方式。

2、垃圾回收 性能指标(吞吐量、暂停时间、内存占用)

(1)常见性能指标:

【吞吐量:】
    吞吐量指 代码运行时间 占 总运行时间的比例。
注:
    总运行时间 = 代码运行时间 + 内存(垃圾)回收时间
    比如: JVM 运行总时间为 100 分钟,垃圾回收时间为 1 分钟,那么吞吐量就为 99%。

【垃圾收集开销:】
    垃圾收集开销指 内存(垃圾)回收时间 占 总运行时间的比例。

【暂停时间(停顿时间):】
    暂停时间指 每次执行垃圾回收时,应用程序线程被暂停的时间(STW 时间)。

【收集频率:】
    收集频率指 相对于应用程序执行,发生垃圾回收的次数(频率)。

【内存占用:】
    Java 堆内存占用大小。

(2)重要指标
  一般来说,吞吐量、内存占用、暂停时间 是衡量 GC 的三大标准,但是一般不能同时满足三个条件。但随着硬件的提升,内存占用的影响相对较小。所以一般还是抉择 吞吐量 与 暂停时间。
  若以 高吞吐量 优先,则必须降低 内存回收 的频率(减少线程切换导致的 时间消耗),但是这样会导致每次 内存回收 导致的暂停时间 变长。
  若以 低暂停时间 优先,则只能频繁进行 内存回收(多次少量),但是这样会导致 频繁切换线程 增加时间消耗。
  高吞吐量、低暂停时间 处于一种矛盾状态,现在一般标准为:在保证最大吞吐量的情况下降低暂停时间。
比如:10 秒进行一次垃圾回收且每次停顿 100 毫秒,现在改为 5 秒进行一次回收且每次停顿 70 毫秒,虽然暂停时间 从 100 毫秒 下降到 70 毫秒,但是垃圾收集频率增加了,若简单的按 10 秒计算,5 秒回收一次导致 总垃圾回收时间为 140 毫秒,从而导致 吞吐量降低。

3、七款经典的垃圾收集器

(1)垃圾收集器分类
  串行垃圾收集器:Serial、Serial Old。
  并行垃圾收集器:ParNew、Parallel Scavenge、Parallel Old。
  并发垃圾收集器:CMS、G1。

(2)垃圾收集器 与 垃圾分代 的关系
  新生代垃圾收集器:Serial、ParNew、Parallel Scavenge。
  老年代垃圾收集器:Serial Old、Parallel Old、CMS。
  整堆收集器:G1。

(3)垃圾收集器组合
  不同垃圾收集器有不同的优缺点,没有更完美的垃圾收集器,只有更合适的垃圾收集器。
  由于 Java 使用场景很多(比如:移动端、服务端等),针对不同的场景,使用不同的垃圾收集器,可以提高垃圾回收的性能。

如下图(图片来源于网络)为各个垃圾收集器的组合关系:

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

注:
  各收集器之间的连线表示 可以组合使用。比如:Serial 与 Serial Old,ParNew 与 CMS 等。
  红色虚线表示移除组合。由于 JDK 版本更新,会废弃、取消一些 垃圾回收器组合。在 JDK8 中 Serial + CMS、ParNew + Serial Old 方式被声明为 废弃,在 JDK 9 中被弃用。
  绿色虚线表示弃用组合。在 JDK14 中废弃 Parallel Scavenge + Serial Old 方式。
  黄色框表示删除。在 JDK14 中删除 CMS 垃圾回收器。

(4)查看默认的垃圾收集器

【参数:】
    -XX:+PrintCommandLineFlags      用于查看命令行参数以及使用的 垃圾回收器

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

【工具:】
    jps                         用于查看 JVM 进程ID
    jinfo -flags 进程ID         用于查看 JVM 的状态(所有参数)
    jinfo -flag [参数] 进程ID    用于查看是否存在某参数

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

4、串行垃圾收集器 -- Serial、Serial Old 收集器

(1)Serial 收集器
  Serial 收集器是最基本的垃圾收集器,JDK1.3 之前用于回收 新生代的唯一选择。其采用 复制算法、串行回收、以及 STW 机制 实现内存回收。
  Serial Old 收集器用于执行 老年代回收,其采用 标记-整理算法、串行回收、以及 STW 机制。

(2)工作流程图(图片来源于网络)
  此收集器属于 单线程收集器,执行垃圾回收时,会暂停用户线程(STW)。
注:
  STW 是由 JVM 后台自动发起、完成的,即在用户不可知、不可控的情况下 将用户线程暂停、启动(若 STW 时间过长,会导致程序卡顿、使用户体验差)。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(3)优缺点:
  简单而高效。在单核 CPU 下,由于没有线程交互的开销,专门进行垃圾回收操作从而执行效率高。
  适用于 Client 模式下的虚拟机。

(4)常见参数

【参数:】
    -XX:+UseSerialGC
注:
    该参数指定 年轻代、老年代 均使用 串行收集器。
    即 年轻代使用 Serial GC,老年代使用 Serial Old GC。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

5、并行垃圾收集器 -- ParNew 收集器

(1)ParNew 收集器
  ParNew GC 本质上属于 Serial GC 的多线程版本,除了采用 并行回收 的方式进行内存回收外,两款收集器差别不大。
注:
  Par 是 Parallel 的缩写,New 指的是处理 年轻代。

(2)工作流程图(图片来源于网络)
  此收集器属于 多线程收集器,执行垃圾回收时,会暂停用户线程(STW)。
  由于 年轻代 回收次数频繁,采用并行方式回收 效率高。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(3)优缺点
  PraNew 收集器 若运行在 多 CPU 环境下,可以充分利用 多 CPU 等硬件优势,完成垃圾回收,此时效率比 Serial 收集器高。
  但若运行在 单核 CPU 环境下,由于并行导致 CPU 频繁线程切换产生额外开销,从而效率反而比不上 Serial 收集器。

(4)常见参数

【参数:】
    -XX:+UseParNewGC
注:
    手动指定使用 ParNew 收集器作为年轻代收集器(ParNew + SerialOld)。

    -XX:+UseConcMarkSweepGC
注:
    可以与 CMS 一起使用,添加参数后,指定 ParNew 收集器 为年轻代收集器。

    -XX:ParallelGCThreads
注:
    设置线程数量,默认与 CPU 核心数相同,比如:-XX:ParallelGCThreads=4。

6、并行垃圾收集器 -- Parallel Scavenge 、Parallel Old 收集器

(1)Parallel Scavenge 收集器
  Parallel Scavenge 是 年轻代收集器,JDK 1.4 时出现,其采用 复制算法、并行回收、以及 STW 机制。
  Parallel Old 是 老年代收集器,JDK 1.6 后出现并用来替代 Serial Old 收集器,其采用 标记-整理算法、并行回收、以及 STW 机制。

(2)Parallel Scavenge 与 ParNew 收集器不同之处:
  Parallel Scavenge 可以通过参数控制 吞吐量。所以有时也称其为 吞吐量优先的 垃圾收集器。
  Parallel Scavenge 可以通过参数设置 自适应调节策略,动态提供合适的 暂停时间以及吞吐量。

(3)工作流程图
  年轻代、老年代 均采用并行回收。
  JDK 8 默认使用此组合进行垃圾回收。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(4)优缺点
  高吞吐量可以高效利用 CPU 执行程序任务,适用于 后台计算且不用太多交互 的任务(比如:订单处理、批量处理等)。

(5)常见参数

【参数:】
     -XX:+UseParallelGC  / -XX:+UseParallelOldGC
注:
    JDK8 默认使用,上面两个参数任选其一即可。
    手动指定年轻代 使用 Parallel Scavenge 进行垃圾回收。
    老年代使用 Parallel Old 进行垃圾回收。

     -XX:ParallelGCThreads
注:
    设置垃圾收集线程数,一般与 CPU 数量相同,以避免线程过多引起 CPU 阻塞从而影响垃圾回收。

    -XX:MaxGCPauseMillis
注:
    设置垃圾收集器 最大停顿时间(STW 时间),单位为 毫秒。
    谨慎使用该值,为了控制停顿时间,收集器执行时可能 会调整 Java 堆大小或者 其他参数。

    -XX:GCTimeRatio
注:
    设置 垃圾收集时间 占总时间的比例,可以用来修改 吞吐量。
    取值范围为 0~100,默认为 99,即 垃圾回收最大时间为 1%-XX:+UseAdaptiveSizePolicy
注:
    开启自适应调节策略(默认开启)。
    此模式下,年轻代大小(-Xmn)、Eden 区 与 Survivor 区比例(-XX:SurvivorRatio) 等参数会自动调整,
    从而达到 堆大小、吞吐量、停顿时间 三者平衡。

7、并发垃圾收集器 -- Concurrent Mark Sweep(CMS) 收集器

(1)CMS 收集器
  JDK 1.5,HotSpot 推出真正意义上的并发收集器 -- CMS 收集器,第一次实现 垃圾收集线程 与 用户线程 同时工作。
  CMS 关注点为 尽可能缩短 垃圾收集的暂停时间(STW 时间),暂停时间越短,响应速度越高,从而提高用户体验(适用于 交互性强的程序)。
  CMS 采用 标记-清除算法、并发回收、以及 STW 机制。
注:
  CMS 不能与 Parallel Scavenge 一起工作,使用 CMS 作为老年代收集器时,年轻代收集器只能从 ParNew 或者 Serial 中选择一个。
  JDK9 将 CMS 标记为 Deprecate,JDK 14 已经移除 CMS。

(2)工作流程图
CMS 工作分为 四个部分:
  初始标记(Initial Mark)阶段。
  并发标记(Concurrent Mark)阶段。
  重新标记(Remark)阶段。
  并发清除(Concurrent Sweep)阶段。
其中:
  初始标记阶段、重新标记阶段 仍然需要 STW(垃圾回收器一般只能尽可能缩短 STW 时间,无法完全消除 STW)。
  并发标记阶段、并发清除阶段 虽然耗时但不需要 STW,从整体上看,属于低暂停时间的。
注:
  由于用户线程不中断,所以 CMS 进行回收时应该保证有足够内存可用,即当老年代内存使用达到阈值时,便开始回收,当内存不足时,会出现并发失败(Concurrent Mode Failure),此时虚拟机将会临时启用 Serial Old 收集器作为预备方案 重新进行 老年代的垃圾回收,导致停顿时间长。
  JDK5 默认阈值为 68%,JDK6 之后为 92%,可以通过 -XX:CMSInitiatingOccupancyFraction 参数进行设置。

Step1:初始标记阶段:
  此阶段 所有工作线程均会出现 STW,
  主要用于 标记出 GC Roots 可以直接关联到的对象,速度很快。

Step2:并发标记阶段。
  此阶段主要为 GC Roots 的直接关联对象开始遍历整个对象链的过程,虽然耗时较长,但是可以与垃圾收集线程一起并发执行。

Step3:重新标记阶段。
  此阶段 所有工作线程均会出现 STW,
  由于并发标记阶段,工作线程并未暂停 可能产生一些 标记变动的对象,此阶段主要任务就是标记出这部分对象,一般耗时稍长,但远小于 并发标记阶段 时间。

Step4:并发清除阶段。
  此阶段主要用于 清理对象、释放空间内存。采用标记-清除算法,不需要移动存活对象,但是会产生内存碎片。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(3)优缺点:
优点:
  支持并发收集、低暂停时间(STW)。

缺点:
  会产生内存碎片。内存空间无法分配大对象时,会提前触发 Full GC。
  程序执行速度可能下降。并发时不停顿用户线程,但会占用一些线程进行垃圾回收((默认垃圾回收线程计算为:(处理器核心数 + 3)/ 4)),从而导致程序变慢,CPU 核心数不够时,程序运行速度将会极大程度降低。
  无法处理浮动垃圾。浮动垃圾指的是 并发标记、并发清除阶段 用户线程运行 产生的垃圾对象,这些垃圾对象出现在 垃圾标记过程结束 后,此次垃圾回收无法被再次标记,即只能在下一次 GC 时被回收。

(4)常见参数

【参数:】
    -XX:+UseConcMarkSweepGC
注:
    手动指定老年代使用 CMS 收集器,年轻代使用 ParNew 收集器。
    即 ParNew(年轻代回收) + CMS(老年代回收)+ Serial Old(老年代预备回收方案)

    -XX:CMSInitiatingOccupancyFraction
注:
    设置堆内存使用率阈值,达到阈值开始回收。
    JDK 5 及以前默认为 68%。JDK 6 及以后默认为 92%。
    若内存增长缓慢,可以设置较高阈值,降低 CMS 执行频率。
    若内存增长迅速,可以设置较低阈值,增加 CMS 执行频率,以避免频繁触发 Serial Old GC。

     -XX:+UseCMSCompactAtFullCollection
注:
    用于指执行完 Full GC 后进行 内存压缩整理,避免内存碎片产生,但内存压缩过程无法并发执行,所以可能导致停顿时间变长。

    -XX:CMSFullGCsBeforeCompaction
注:
    设置在执行多少次 Full GC 后进行内存空间压缩整理。

8、并发垃圾回收器 -- Garbage First(G1)收集器

(1)Garbage First 收集器
  随着业务庞大、复杂,不断的对 GC 优化,为了适应不断扩大的内存 和 不断增加的处理器数量、进一步降低暂停时间、兼顾良好的吞吐量,在 JDK7 时引入了 G1 收集器。
  G1 是一款面向服务端应用、低暂停时间的垃圾收集器,只要针对 多核 CPU 以及 大容量内存的机器,在 JDK 7 中正式启用,并在 JDK 9 中作为默认垃圾回收器。在减少暂停时间的基础上提高吞吐量(用来替代 CMS)。
  G1 将堆内存空间 划分为若干大小相同的独立的 Region(默认划分为 2048 个内存大小相同的区域),通过参数 -XX:G1HeapRegionSize 可以设定内存大小,范围为 1MB ~ 32MB 且为 2 的 N 次幂。一个 region 可能属于 Eden、Survivor、Old 内存区域,但是每一个 region 一次只能代表一个内存区域。新增一个 Humongous 内存区域,用于存储大对象(超过 1.5 region 大小即为大对象)。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(2)为什么叫 G1?
  G1 将堆内存分割成很多不相关的区域(Region),使用不同的 Region 表示 Eden、Survivor0、Survivor1、老年代等。其跟踪各个 Region 里面垃圾堆积的价值(回收所获得的空间大小以及回收所需时间),并在后台对这些值维护一个优先列表,每次回收优先级大的 Region,也即优先收集垃圾价值最大的区域,所以叫垃圾优先(Garbage First)。

(3)工作流程图
详见参考:
  https://www.jianshu.com/p/7dd309cc3442
  https://blog.csdn.net/coderlius/article/details/79272773

GC 回收流程主要为三步:
  年轻代 GC(Young GC)。
  混合 GC(Mixed GC)。
  内存分配不足时会触发 Full GC。

Step1:年轻代 GC
  Young GC 是 STW、并行执行的。
  Eden 区满后触发 Young GC,Eden 区对象移动到 Survivor 区,大对象直接进入 Old 区,Survivor 区满足年龄条件后,同样可以进入 Old 区。

Step2:混合 GC。
  Mixed GC 分为两个阶段:
    并发标记阶段。
    拷贝存活对象阶段。

  并发标记过程与 CMS 并发收集过程类似,稍有区别。同样超过内存占用阈值时将会触发(使用参数可以设置,默认为 -XX:InitiatingHeapOccupancyPercent=45),阈值的区别在于 G1 指的是 整堆的内存占用率,CMS 指的是 老年代的占用率。

并发标记流程:
  初始标记。
  并发标记。
  重新标记。
  清理阶段。

【初始标记:】
    此阶段 所有工作线程均会出现 STW,
    主要用于 标记出 GC Roots 可以直接关联到的对象,借用了 Young GC 的暂停时间进行标记。

【并发标记:】
    此阶段主要为 GC Roots 的直接关联对象开始遍历整个对象链的过程,虽然耗时较长,但是可以与垃圾收集线程一起并发执行。

【重新标记:】
    此阶段 所有工作线程均会出现 STW,
   标记出并发阶段发生变化的对象。

【清理阶段:】
    此阶段 所有工作线程均会出现 STW,
    找到空闲的 Region 并回收到 可分配的 Region 中。
注:
    此阶段只回收 完全空闲的 Region,若有存活对象的 Region,将会在 Mixed GC 中回收。    

拷贝存活对象阶段(Evacuation):
  此阶段 所有工作线程均会出现 STW,
  更新 Region 数据,对 Region 回收价值、回收成本 排序,根据 用户设置的停顿时间 选择 多个 Region (所有年轻代、部分老年代)构成回收集(Collection Set、CSet),将存活对象复制到空的 Region 中,并清理旧的 Region。

学习一下 JVM (三) -- 了解一下 垃圾回收-LMLPHP

(4)常见参数

【参数:】
    -XX:+UseG1GC
注:
    手动指定使用 G1 收集器执行垃圾回收。

    -XX:G1HeapRegionSize
注:
    设置每个 Region 大小,值为 2 的幂,范围为 1MB ~ 32MB。

    -XX:MaxGCPauseMillis
注:
    设置最大 GC 停顿时间,默认 200 ms。

    -XX:InitiatingHeapOccupancyPercent
注:
    设置触发 GC 的堆占用率阈值,默认 45%。   

五、GC 日志分析

  通过阅读 GC 日志信息,可以快速了解当前 JVM 内存分配与回收策略。

1、常用参数

【参数:】
    -XX:+PrintGC            输出 GC 日志信息。
    -XX:+PrintGCDetails     输出 GC 详细信息(最后输出堆内存分配、使用情况)。
    -XX:+PrintGCTimeStamps  输出 GC 时间戳。
    -XX:+PrintGCDateStamps  输出 GC 时间戳(日期格式)。
    -XX:+PrintHeapAtGC      在 GC 执行前后打印出 堆的信息。
    -Xloggc:logs/gc.log     指定日志输出路径。

2、日志分析

【举例:】
/**
 * JVM 参数设置 -XX:+PrintGCDetails -Xloggc:logs/gc.log -Xms5m -Xmx5m
 *
 */
public class JVMDemo {

    public static void main(String[] args) {
        byte[] buffer = new byte[1 * 1024 * 1024];
        byte[] buffer2 = new byte[1 * 1024 * 1024];
        System.gc();
    }
}

【输出:】
Java HotSpot(TM) 64-Bit Server VM (25.92-b14) for windows-amd64 JRE (1.8.0_92-b14), built on Mar 31 2016 21:03:04 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16646060k(8616984k free), swap 19136428k(6872820k free)
CommandLine flags: -XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
0.123: [GC (Allocation Failure) [PSYoungGen: 1018K->485K(1536K)] 1018K->557K(5632K), 0.0009012 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.145: [GC (System.gc()) [PSYoungGen: 983K->485K(1536K)] 3103K->2661K(5632K), 0.0005754 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.146: [Full GC (System.gc()) [PSYoungGen: 485K->0K(1536K)] [ParOldGen: 2176K->2589K(4096K)] 2661K->2589K(5632K), [Metaspace: 3088K->3088K(1056768K)], 0.0040756 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
Heap
 PSYoungGen      total 1536K, used 51K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 5% used [0x00000000ffe00000,0x00000000ffe0ce68,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 2589K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 63% used [0x00000000ffa00000,0x00000000ffc875f0,0x00000000ffe00000)
 Metaspace       used 3096K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 340K, capacity 388K, committed 512K, reserved 1048576K

【分析:】
重点关注一下三行:
    0.123: [GC (Allocation Failure) [PSYoungGen: 1018K->485K(1536K)] 1018K->557K(5632K), 0.0009012 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    0.145: [GC (System.gc()) [PSYoungGen: 983K->485K(1536K)] 3103K->2661K(5632K), 0.0005754 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    0.146: [Full GC (System.gc()) [PSYoungGen: 485K->0K(1536K)] [ParOldGen: 2176K->2589K(4096K)] 2661K->2589K(5632K), [Metaspace: 3088K->3088K(1056768K)], 0.0040756 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
其中:
    GC、Full GC 表示停顿类型,GC 为年轻代收集, Full GC 为整堆收集(发生 STW,收集堆 与 方法区)。
    (Allocation Failure) 表示引起 GC 原因为年轻代中没有足够内存存储新数据。
    (System.gc()) 表示引起 GC 原因为触发了 System.gc()。
    [PSYoungGen: 1018K->485K(1536K)] 表示回收年轻代,回收前内存 1018K,回收后 485K,年轻代总大小 1536K。
    [ParOldGen: 2176K->2589K(4096K)] 表示回收老年代,回收前内存 2176K,回收后内存 2589K,老年代总大小 4096K。
    3103K->2661K(5632K), 0.0005754 secs 表示回收年轻代、老年代,回收前 3103K,回收后 2661K,年轻代、老年代总大小 5632K。GC 时间为 0.0005754 秒。
    [Times: user=0.00 sys=0.00, real=0.00 secs] 表示用户态回收耗时,系统内核态回收耗时,实际耗时。

注:
    不同收集器,年轻代、老年代 名字不同。
    收集器             年轻代             老年代
    Serial GC          DefNew            Tenured
    ParNew GC          ParNew            Tenured
    Parallel GC        PSYoungGen        ParOldGen
09-02 02:25