不可错过的JVM深度好文!-纯干货详解JVM垃圾回收

JVM-垃圾回收

1. 垃圾回收相关概述

1.1 什么是垃圾

​ 垃圾指的是在运行程序中没有任何指针(或引用)指向的对象,这个对象就是须要回收的垃圾。 若是不及时对内存中的垃圾进行清理,那么这些垃圾对象所占用的内存空间一直保留到应用程序结束,被保留的空间没法被其余对象使用。可能会致使内存溢出。php

​ 对于高级语言来讲,若是不进行垃圾回收,由于不断分配内存而不进行回收,内存迟早会被消耗完。除了释放没有用的对象,垃圾回收也能够清除内存里的碎片,碎片整理将所占用的堆内存移动到堆的一端,便于JVM将整理出内存分配给新的对象。特别是大的对象,可能须要一块连续的大的内存空间。html

1.2 什么是GC

​ **垃圾回收(Garbage Collection)**做为一⻔实用而又􏰀要的技术,能够说拯救了无数苦于内存管理的程序员。尽管不少人认为,GC技术走进大众的视􏰁,可能是源于Java语言的崛起,可是GC技术自己却至关的古老。早在1960年,Lisp之父John McCarthy已经在其论文中发布了GC算法,Lisp语言也是第 一个实现GC的语言。java

​ 在 GC 最开始设计时,人们在思考 GC 时就须要完成三件事情:python

  1. 哪些内存须要进行回收?
  2. 何时对这些内存进行回收?
  3. 如何进行回收?

​ 垃圾回收与“java面向对象编程”同样是java语言的特性之一;它与“ c/c++语言”最大区别是不用手动调用 free() 和 delete() 释放内存。GC 主要是处理 JavaHeap ,也就是做用在 Java虚拟机 用于存放对象实例的内存区域,(Java堆又称为GC堆)。JVM可以完成内存分配和内存回收,虽然下降了开发难度,避免了像C/C++直接操做内存的危险。但也正由于太过于依赖JVM去完成内存管理,致使不少Java 开发者再也不关心内存分配,致使不少程序低效、耗内存问题。所以开发者须要主动了解GC机制,充分利用有限的内存的程序,才能写出更高效的程序。c++

在这里插入图片描述

垃圾回收机制仍然在不断的迭代中,不一样的场景对垃圾回收提出了新的挑战。程序员

1.3 STW

Stop-the-World,简称STW,指的是GC事件发生过程当中,会产生应用程序的停顿。停顿是产生时整 个应用程序线程会被暂停,没有任何响应,有点像卡死的感受,这个停顿称为STW。Stop-the-world意味着 JVM因为要执行GC而中止了应用程序(用户线程、工做线程)的执行,而且这种情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程之外,全部线程都处于等待状态直到GC任务完成。web

在这里插入图片描述
​ STW事件和采用哪款GC无关,全部的GC都有这个事件。哪怕是G1也不能彻底避免Stop-the-world 状况发生,只能说垃圾回收器愈来愈优秀,回收效率愈来愈高,尽量缩短了暂停时间。算法

​ STW是JVM在后台自动发起和自动完成的,在用户不可⻅的状况下,把用户正常的工做线程所有停掉。编程

​ 随着应用程序愈来愈复杂,每次GC不能保证应用程序的正常运行。而常常形成STW的GC跟不上实际的需求,因此才须要不断对GC进行优化。事实上,GC优化不少时候就是指减小Stop-the-world发生的时间从而使系统具备高吞吐 、低停顿的特色c#

1.4 并行与并发

并发(Concurrent)

​ 在操做系统中,是指一个时间段中有个几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。

​ 并发并非真正意义上的"同时进行",只是CPU把一个时间段划分红几个时间片断(时间区间),而后在这几个时间区间之间来回切换,因为CPU处理的速度很是快,只要时间间隔处理得当,便可让用户感受是多个应用程序是同时进行的。

在这里插入图片描述

并行**(Parallel)**

​ 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU能够执行另外一个进程,两个进程互不抢占CPU资源,能够同时进行,咱们称之为并行(Parallel);

​ 其实决定并行的因素不是CPU的数􏰂量,而是CPU的核心数􏰂量,好比一个CPU多个核能够并行。

​ 适合科学计算,后台处理等弱交互场景。

在这里插入图片描述

两者对比:

并发,指的是多个事情,在同一时间段内同时发生了。

并行,指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。

并行的多个任务之间是不互相抢占资源的。

只有在多个CPU或者一个CPU多核的状况中,才会发生并行。 不然,看似同时发生的事情,其实都是并发执行的。

1.5 GC分类

JVM在进行回收时,是针对不一样的内存区域进行回收的,大多数的回收指的是对新生代的回收。

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式。其中又分为

    • 新生代的回收:(Minor GC/Young GC),只收集新生代的GC

    • 老年代的回收:(Major GC/Old GC),只收集老年代的GC。

      ​ 目前只有CMS的concurrent collection是这个模式,只收集老年代。

    • Mixed GC:收集整个young gen以及部分old gen的GC。

      ​ 只有G1有这个模式。

  • Full GC:收集整个堆,包括young gen、old gen、perm gen(若是存在的话)等全部部分的模式。

​ Major GC一般是跟full GC是等价的,收集整个GC堆。但由于HotSpot VM发展了这么多年, 外界对各类名词的解读已经彻底混乱了,当有人说“major GC”的时候必定要问清楚他想要指的是上面的full GC仍是old GC。

约定: 新生代/新生区/年轻代 养老区/老年区/老年代/年老代 永久区/永久代

1.6 GC触发条件

最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:

  • 年轻代(Minor GC)触发条件:

​ 通常状况下,全部新生成的对象首先都是放在新生代的。新生代内存按照 8:1:1 的比例分为一 个eden区和两个survivor(from survivor,to survivor)区,大部分对象在Eden区中生成。

​ 在进行垃圾回收时,先将eden区存活对象复制到from survivor区,而后清空eden区,当这个from survivor区也满了时,则将eden区和from survivor区存活对象复制到to survivor区,而后清空eden和这个from survivor区,此时from survivor区是空的,而后交换from survivor区和to survivor区的⻆色(即下次垃圾回收时会扫描Eden区和to survivor区),即保持from survivor区为空,如此往复。

​ 特别地,当to survivor区也不足以存放eden区和from survivor区的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC叫作MinorGC,MinorGC发生频率比较高,不必定等 Eden区满了才触发。

​ Minor GC触发比较频繁,通常回收速度也是比较快的。Minor GC会引起STW,暂停用户线程,等待垃圾回收完毕后,用户线程才会恢复。

在这里插入图片描述

  • 老年代(Major GC)触发条件:

(1) 由Eden区、from survivor区向to survivor区复制时,对象大小大于to survivor可用内存,则把该对象转存到老年代,会先尝试触发Minor GC,若是以后空间仍是不足,则会触发Major GC。

(2) 若是Major GC后仍是不足,就会OOM。

(3) 发生Major GC,一般伴随Minor GC,但这并非绝对的,Parallel Scavage这种收集器就有直接进行Major GC的策略过程。

说明:Major GC的速度通常会比Minor GC的速度慢10倍以上。

  • FULL GC触发条件:

(1)System.gc()方法的调用

​ 此方法的调用是建议JVM进行Full GC,虽然只是建议而非必定,但不少状况下它会触发Full GC,从而增长Full GC的频率,也即增长了间歇性停顿的次数。建议能不使用此方法就别使用,让虚拟机本身去管理它的内存,可经过-XX:+DisableExplicitGC来禁止RMI(Java远程方法调用)调用 System.gc。

(2)老年代空间不足

​ 老年代空间只有在新生代对象转入及建立为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出以下错误: java.lang.OutOfMemoryError: Java heap space,为避免以上两种情况引发的FullGC,调优时应尽􏰂作到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要建立过大的对象及数组。

(3)方法区空间不足

​ JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常􏰂量、静态变􏰂等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用 CMS GC的状况下也会执行Full GC。若是通过Full GC仍然回收不了,那么JVM会抛出以下错误信 息:

​ java.lang.OutOfMemoryError: PermGen space

​ 为避免Perm Gen占满形成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

(4)经过Minor GC后进入老年代的平均大小大于老年代的可用内存

若是发现统计数听说以前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC

(5)由Eden区、from survivor区向to survivor区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

2. 检测垃圾

​ 在堆里放着几乎全部的java对象实例,在GC执行垃圾回收以前,首先须要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收,释放掉其所占用的内存空间,所以这个过程咱们能够称为垃圾标记阶段

​ 那么在JVM中到底是如何标记一个对象是死亡的呢?简单地说,当一个对象已经再也不被任何的存活对象继续引用时,就能够判断为已经死亡。

​ 判断对象存活通常有两种方式:引用计数算法可达性分析算法

2.1 引用计数算法(Reference Counting)

​ 引用计数算法:经过判断对象的引用数􏰂来决定对象是否能够被回收。

​ 引用计数算法是垃圾收集器中的早期策略。在这种方法中,堆中的每一个对象实例都有一个引用计数。当一个对象被建立时,且将该对象实例分配给一个引用变􏰂量,该对象实例的引用计数设置为 1。当 任何其它变􏰂被赋值为这个对象的引用时,对象实例的引用计数加 1(a = b,则b引用的对象实例的计数器加1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减1。特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减1。任何引用计数为0的对象实例能够被看成垃圾收集。

​ 引用计数收集器能够很快的执行,而且交织在程序运行中,对须要不被⻓时间打断的实时环境比较 利,但其很难解决对象之间相互循环引用的问题。以下面示例所示,对象objA和objB之间的引用计数永远不可能为0,那么这两个对象就永远不能被回收。

在这里插入图片描述

/** * -Xms10m -Xmx10m -XX:+PrintGCDetails * 证实java使用的不是引用计数器算法 */
public class ReferenceCountGC {
    public Object instance = null;
    private byte[] bigObject = new byte[1024*1024];
    public static void main(String[] args){
        ReferenceCountGC objA = new ReferenceCountGC ();
        ReferenceCountGC objB = new ReferenceCountGC ();
// 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为 0 objB.instance = objA;
        objA.instance = objB;
        objA = null;
        objB = null;
        System.gc(); //经过注释,打开或关闭垃圾回收的执行
        }
    }

​ 上述代码最后面两句将objA和objB赋值为null,也就是说objA和objB指向的对象已经不可能再被访问,可是因为它们互相引用对方,致使它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

优势:

  • 实现简单,垃圾对象便于标识;
  • 断定效率高,回收没有延迟性。

缺点:

  • 它须要单独的字段存储计数器,这样的作法增长了存储空间的开销。
  • 每次赋值都须要更新计数器,伴随这加法和减法操做,这就增长了时间开销。
  • 致命缺陷,即没法处理循环引用的状况。致使在java的垃圾回收器中没有使用这类算法。

扩展知识点

​ java并无选择引用计数,是由于其存在一个基本的难题,也就是很难处理循环引用关系。引用计数算法,是不少语言的资源回收选择,例如python,它更是同时支持引用计数和垃圾收集机制。 Python如何解决循环引用?

  • ​ 手动解除:很好理解,就是在合适的时机,解除引用关系。
  • ​ 使用弱引用weakref,weakref是Ptyhon提供的标准库,旨在解决循环引用。

2.2 可达性分析算法(Rearchability Analysis)

2.2.1 概述

​ 相对于引用计数算法,这里的可达性分析是java、c# 选择的。这种类型的垃圾收集一般也叫追踪性垃圾收集****(Tracing Garbage Collection)

​ 可达性分析算法是经过判断对象的引用链是否可达来决定对象是否能够被回收。

​ 可达性分析算法是从离散数学中的图论引入的,程序把全部的引用关系看做一张图,经过一系列的名为 “GC Roots” 的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链 (Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来讲就是从 GC Roots 到这个对象不可达)时,则证实此对象是不可用的,以下图所示。

在这里插入图片描述

在java中,可做为 GC Root 的对象包括如下几种:

  1. 虚拟机栈(栈帧中的局部变􏰂表)中引用的对象;

​ 好比:各个线程被调用的方法中使用的参数、局部变􏰂量等。

  1. 方法区中类静态属性引用的对象;

​ 好比:java类的引用类型静态变量􏰂

3)方法区中常􏰂引用的对象;

​ 好比:字符串常􏰂池(String Table)里的引用

4)本地方法栈中Native方法引用的对象;

5)全部被同步锁synchronized持有的对象;

  1. java虚拟机内部的引用;

​ 好比:基本数据类型对应的Class对象,一些异常对象(如:NullPointerException、 OutOfMemoryError),系统类加载器

7)反映java虚拟机内部状况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

​ 因为Root采用栈方式存放变􏰂和指针,因此若是一个指针,它保存了堆内存里面的对象地址,可是本身又不存放在堆内存里面,那它就是一个Root。

2.2.2 代码演示

下面以一段代码来简单说明一下

class RearchabilityTest {
	private static A a = new A(); // 静态变量􏰂
	public static final String CONTANT = "I am a string"; // 常量􏰂 
	public static void main(String[] args) {
	A innerA = new A(); // 局部变􏰂量 
	}
}
class A { 
}

在这里插入图片描述

​ 首先,类加载器加载RearchabilityTest类,会初始化静态变量􏰂a,将常􏰂量引用指向常量􏰂池中的字符串,完成RearchabilityTest类的加载; 而后main方法执行,main方法会入虚拟机方法栈,执行main方法会在堆中建立A的对象,并赋值给局部变􏰂量innerA。

​ 此时GC Roots状态以下:

在这里插入图片描述

​ 当main方法执行完出栈后,变为:

在这里插入图片描述

​ 第三个对象已经没有引用链可达GC Root。此时,第三个对象被第一次标记。

2.2.4 使用MAT查看GC Roots

MAT是一个强大的内存分析工具,能够快捷、有效地帮助咱们找到内存泄露,减小内存消耗分析工具。

MAT是Memory Analyzer tool的缩写,是一种快速,功能丰富的Java堆分析工具,能帮助你查找内存泄漏和减小内存消耗。不少状况下,咱们须要处理测试提供的hprof文件,分析内存相关问题,那么 MAT也绝对是不二之选。

MAT安装有两种方式,一种是以eclipse插件方式安装,一种是独立安装。在MAT的官方文档中有相应的安装文件下载,下载地址为:https://www.eclipse.org/mat/downloads.php

在这里插入图片描述

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
public class GCRootsTest {
    public static void main(String[] args) {
    List<Object> numList = new ArrayList<>();
    Date birth = new Date();
    for (int i = 0; i < 100; i++)
    {
        numList.add(String.valueOf(i));
        try
        {
            Thread.sleep(10);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    System.out.println("数据添加完毕,请下一步操做:");
    new Scanner(System.in).next();
    numList = null;
    birth = null;
    System.out.println("numList、birth已置空,请下一步操做:");
    new Scanner(System.in).next();
    System.out.println("结束");
    }
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3. 垃圾收集算法

​ 当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。

目前在JVM中比较常⻅的三种垃圾回收算法是标记-清除算法(Mark-Sweep)、复制算法(Copying)、标记-压缩算法(Mark-Compact)

3.1 标记清除算法

​ 标记-清除算法(Mark-Sweep)是一种很是基础和常⻅的垃圾收集算法,该算法被J.McCarthy等人在 1960年提出并应用于Lisp语言。

​ 清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收,以下图所示。

在这里插入图片描述

  • ​ 标记:GC从引用根节点开始遍历,标记全部被引用的对象。通常是在对象的Header中记录。
  • ​ 清除:GC对堆内存从头至尾进行线性的遍历,若是发现某个对象再其Header中没有标记为可达对象,则将其回收。

标记-清除算法的主要不足有:

  • ​ 效率问题:标记和清除两个过程的效率都不高;
  • ​ 在进行GC的时候,需中止整个应用程序,致使用户体验性差;
  • ​ 空间问题:标记-清除算法不须要进行对象的移动,而且仅对不存活的对象进行处理,所以标记清除以后会产生大􏰂不连续的内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。

在这里插入图片描述
在这里插入图片描述

3.2 复制算法

​ 为了解决标记-清除算法在垃圾收集效率方面的缺陷, M. L. Minsky 于1963 年发表了著名的论 文“一种使用双存储区的 Lisp 语言垃圾收集器( A LISP Garbage Collector Algorithm Using Serial Secondary Storage )”。 M. L. Minsky 在该论文中描述的算法被人们称为复制算法(Copying),它也被 M. L. Minsky 本人成功地引入到了 Lisp 语言的一个实现版本中。

​ 复制算法别出心裁地将堆空间一分为二,并使用简单的复制操做来完成垃圾收集工做,这个思路至关有趣。

​ 复制算法将可用内存按容􏰂划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完 了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,好比新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。该算法示意图以下所示:

在这里插入图片描述

应用场景:

​ 事实上,如今商用的虚拟机都采用这种算法来回收新生代。由于研究发现,新生代中的对象每次回收都基本上只有**10%**左右的对象存活,因此须要复制的对象不多,效率还不错。不适合存活􏰂对象比较多的场景。

优势:

  • 没有标记和清除过程,实现简单,运行高效
  • 复制过去之后保证空间的连续性,不会出现"碎片" 问题。

缺点:

  • 此算法的缺点也是很明显,就是须要两倍的内存空间
  • 若是对象的存活率很高,咱们能够极端一点,假设是100%存活,那么咱们须要将全部对象都复制一遍,并将全部引用地址􏰀置一遍。复制这一工做所花费的时间,在对象存活率达到必定程度时, 将会变的不可忽视。

3.3 标记整理算法

​ 复制收集算法在对象存活率较高时就要进行较多的复制操做,效率将会变低。更关键的是,若是不想浪费50%的空间,以应对被使用的内存中全部对象都100%存活的极端状况,因此在老年代通常不能直接选用这种算法。

​ 标记-整理算法或标记-压缩算法(Mark-Compact)是标记-清除算法和复制算法的有机结合。把标记-清除算法在内存占用上的优势和复制算法在执行效率上的特⻓综合起来,这是全部人都但愿看到的结果。不过,两种垃圾收集算法的整合并不像 1 加 1 等于 2 那样简单,咱们必须引入一些全新的思路。 1970 年先后, G. L. Steele , C. J. Cheney 和 D. S. Wise 等研究者陆续找到了正确的方向,标记-整理算法的轮廓也逐渐清晰了起来。

​ 标记-整理算法的标记过程相似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存,相似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代),其做用原理以下图所示。

在这里插入图片描述

​ 标记-整理算法与标记-清除算法最显著的区别是:标记-清除算法不进行对象的移动,而且仅对不存活的对象进行处理;而标记整理算法会将全部的存活对象移动到一端,并对不存活对象进行处理,所以 其不会产生内存碎片。标记-整理算法的做用示意图以下:

在这里插入图片描述

​ 标记-整理算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,所以,也能够把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。

​ 两者的本质差别在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的⻛险决策。

​ 能够看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。 如此一来,当咱们须要给新对象分配内存时,JVM只须要持有一个内存的起始地址便可,这比维护一个空闲列表显然少了许多开销。

优势:

  • 消除了标记-清除算法当中,内存区域分散的缺点,咱们须要给新对象分配内存时,JVM只须要持有一个内存的起始地址便可。
  • 消除了复制算法当中,内存两倍的高额代价。

缺点:

  • 从效率上来讲,标记-整理算法要低于复制算法。
  • 移动对象的同时,若是对象被其余对象引用,则还须要调整引用的地址。
  • 移动过程当中,须要全程暂停用户应用程序。即:STW

对比三种算法

在这里插入图片描述

​ 效率上来讲,复制算法是最快的,可是却浪费了太多的内存。

​ 而为了兼顾上面提到的三个指标,标记**-**整理算法相对来讲更平滑一些,可是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。

3.4 分代收集算法

​ 前面全部这些算法中,并无一种算法能够彻底替代其余算法,它们都具备本身独特的优点和特色。分代收集算法应运而生。

​ 分代收集算法(Generational Collecting),是基于这样的一个事实:不一样的对象生命周期是不同 的。所以不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率。通常是把java堆分红新生 代和老年代,这样就能够根据各个年代的特色使用不一样的回收算法,以提升垃圾回收的效率。

一、新生代(Young Generation)

新生代特色:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。

这种状况适合复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关, 所以很适用于年轻代的回收。而复制算法内存利用率不高的问题,经过hotspot中的两个survivor的设计获得缓解。

二、老年代(Old Generation)

老年代特色:区域较大,对象生命周期⻓、存货效率高,回收不及年轻代频繁

这种状况存在大􏰂存活率高的对象,复制算法明显变得不合适。通常是由标记-清除或者是标记-清除与标记-整理的混合实现。

  • 标记(Mark)阶段的开销与存活对象的数􏰂成正比;
  • 清除(Sweep)阶段的开销与所管理区域的大小成正比相关;
  • 压缩(Compact)阶段的开销与存活对象的数据成正比。

老年代存放的都是一些生命周期较⻓的对象,就像上面所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大不少(大概比例是1:2), 当老年代满时会触发Major GC(Full GC),老年代对象存活时间比较⻓,所以FullGC发生的频率比较低。

三、 永久代(Permanent Generation)

永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,可是有些应 用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候须要设置一个比较大的永久代空间来存放这些运行过程当中新增的类。

分代的思想被现有的虚拟机普遍使用。几乎全部的垃圾回收器都区分新生代和老年代。

3.5 增量式垃圾回收

增􏰂量式垃圾回收并非一个新的回收算法, 而是结合以前算法的一种新的思路。

以前说的各类垃圾回收, 都须要暂停程序, 执行GC, 这就致使在GC执行期间, 程序得不到执行. 所以出现了增量􏰂式垃圾回收, 它并不会等GC执行完, 才将控制权交回程序, 而是一步一步执行, 跑一点, 再跑一 点, 逐步完成垃圾回收**,** 在程序运行中穿插进行。极大地下降了GC的最大暂停时间。

整体来讲,增􏰂量式垃圾回收算法的基础还是传统的标记-清除和复制算法。增􏰂量式垃圾回收经过对线程间冲突的妥善处理,容许垃圾收集线程以分阶段的方式完成标记、清理或复制工做。

缺点:

使用这种方式,因为在垃圾回收过程当中,间断性地还执行了应用程序代码,因此能减小系统的停顿时间。可是,由于线程切换和上下文转换的消耗,会使得垃圾回收的整体成本上升,形成系统吞吐􏰂的 降低。

3.6 分区算法

通常来讲,在相同条件下,堆空间越大,一次GC时所须要的时间也就越⻓,有关GC产生的停顿也越⻓。为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时 间,每次合理回收若干个小区件,而不是整个堆空间,从而减小一次GC所产生的停顿。

分代算法将按照对象的生命周期⻓短划分红两个部分,分区算法将整个堆空间划分红连续的不一样小区间。

每个小区间都独立使用,独立回收。这种算法的好处是能够控制一次回收多个小区间。

在这里插入图片描述

注意:这些都只是基本的算法思路,实际GC实现过程要复杂得多,目前发展中的前沿GC都是复合算法,而且并行和并发兼备。

4. 垃圾收集器

4.1 垃圾收集器分类

垃圾收集器没有在规范中进行过多的规定,能够由不一样的厂商、不一样版本的JVM来实现。因为JDK版本的处于高速迭代过程当中,所以java发展至今已经衍生了众多GC版本。从不一样⻆度分析垃圾收集器, 能够将GC分为不一样的类型。

  1. 按线程数分,能够分为串行垃圾回收器和并行垃圾回收器。

在这里插入图片描述

  • 串行回收指的是在同一时间段只容许有一个CPU用于执行垃圾回收操做,此时工做线程被暂停,直至垃圾收集工做结束。
  • ​ 和串行回收相反,并行收集能够运用多个CPU同时执行垃圾回收,所以提高了应用的吞吐􏰂,不过并行回收仍然与串行回收同样,采用独占式,使用了“Stop-the-world”机制。

​ 在诸如单CPU处理器或者较小内存等硬件场合中,串行回收器的性能表现能够超过并行回收器和并发回收器。因此,串行回收默认被应用在客户端Client模式下的JVM中。

​ 在并发能力较强的CPU上,并行回收器产生的停顿时间要短于串行回收器。

  1. 按照工做模式分,能够分为并发式垃圾回收器和独占式垃圾回收器。
  • 并发式垃圾回收器与应用程序线程交替工做,以尽量减小应用程序的停顿时间。
  • 独占式垃圾回收器一旦运行,就中止应用程序中的全部用户线程,直到垃圾回收过程彻底结束。

在这里插入图片描述

  1. 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。
  • 压缩式垃圾回收器在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
  • 非压缩式的垃圾回收器不进行这不操做,会产生碎片。

在这里插入图片描述

  1. 按工做的内存空间分,又可分为年轻代垃圾回收器和老年代垃圾回收器

4.2 评估GC的性能指标

  • 吞吐量􏰂:运行用户代码的时间占总运行时间的比例 (总运行时间=程序的运行时间+内存回收的时间);
  • 暂停时间:执行垃圾收集时,程序的工做线程被暂停的时间;
  • 内存占用:java堆区所占的内存大小;

吞吐量􏰂就是CPU用于运行用户代码的时间与CPU总消耗的时间的比值,即吞吐量􏰂=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。好比:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那么吞吐􏰂就是99%。这种状况下,应用程序能容忍较高的暂停时间,所以,高吞吐􏰂的应用程序有更⻓的时间基准,快速响应是没必要考虑的。

暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态。好比:GC期间100毫秒的暂停时间意味这在这100毫秒期间内没有应用程序线程是活动的。

**注重吞吐量:**吞吐量􏰂优先,意味着在单位时间内,STW的时间最短:0.2 + 0.2= 0.4s

在这里插入图片描述
**注重低延迟:**暂停时间优先,意味这尽量让单次STW的时间最短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5s

在这里插入图片描述

​ 这三者共同构成一个”不可能三⻆“。三者整体的表现会随着技术进步而愈来愈好。一款优秀的收集器一般最多同时知足其中的两项。简单来讲,主要抓住两点:

  • 吞吐量
  • 暂停时间

​ 在设计(或使用)GC算法时,必须肯定咱们的目标:一个GC算法只可能针对两个目标之一(即只专一于较大吞吐􏰂或最小暂停时间),或尝试找一个两者的折衷。

如今标准,在最大吞吐量优先的状况下,下降停顿时间。

4.3 垃圾收集器发展史

有了虚拟机,就必定有须要收集垃圾的机制,这就是Garbage Collection,对应的产品咱们称之为 Garbage Collector

  • 1999年随着JDK1.3.1 一块儿来的是串行方式的Serial GC,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本。
  • 2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟随着JDK1.4.2 一块儿发布
  • Parallel GC在JDK6以后成为HotSpot默认GC。
  • 2012年,在JDK1.7u4版本中,G1可用。
  • 2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
  • 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏状况下的延迟。
  • 2018年9月,JDK11发布。引入Epsilon垃圾回收器,又被称为"No-Op(无操做)"回收器。同时引入 ZGC(Oracle 发布):可伸缩的低延迟垃圾回收器(Experimental)。
  • 2019年3月,JDK12发布。加强G1,自动返回未用堆内存给操做系统。同时,引入Shenandoah GC(Red Hat 开发):低停顿时间的GC(Experimetal)。
  • 2019年9月,JDK13发布。加强ZGC,自动返回未用堆内存给操做系统。
  • 2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macOS和Windows上的应用。

4.4 经典垃圾回收器

  • 串行回收器:Serial 、 Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel Old
  • 并发回收器:CMS、G1

官方文档参考:

https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

4.5 垃圾回收器的组合关系

​ 若是说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展现了7种做用于不一样分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1 收集器。不一样收集器之间的连线表示它们能够搭配使用。

在这里插入图片描述

​ 为何要有不少收集器,一个不够吗 ?由于java的使用场景不少,移动端、服务器等。因此就须要针对不一样的场景,提供不一样的垃圾收集器,提升垃圾收集的性能。

​ 虽然咱们会对各个收集器进行比较,但并不是为了挑选一个最好的收集器出来。没有一种放之四海而皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器。因此咱们选择的只是对具体应用最合适的收集器。

如何查看默认的垃圾回收器

  • -XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
  • 使用命令行指令: jinfo -flag 相关垃圾回收参数 进程ID
import java.util.ArrayList;
import java.util.List;
/** * -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC */
public class GCUseTest {
    public static void main(String[] args) {
			List<byte[]> list = new ArrayList<>(); 
      while (true){
				byte[] arr = new byte[100]; 
      	list.add(arr);
				try {
						Thread.sleep(10);
				} catch (InterruptedException e) {
						e.printStackTrace(); 
     	 	}
      }
    }
}

输出:

-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=348966912 -XX:MaxTenuringThreshold=6 -XX:OldPLABSize=16 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

4.6 Serial(复制算法)

​ Serial/Serial Old收集器是最基本最古老的收集器,Serial是JDK1.3以前回收新生代惟一的选择。它是一个单线程收集器,而且在它进行垃圾收集时,必须暂停全部用户线程。

​ Serial收集器是做为HotSpot中Client模式下的默认新生代垃圾收集器,采用的是复制算法。Serial Old收集器是针对老年代的收集器,采用的是标记-整理算法。

​ 以下是 Serial 收集器和 Serial Old 收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,全部线程暂停执行,Serial 收集器以单线程,采用复制算法进行垃圾收集工做,收集完以后,用户线程继续开始执行。它的”单线程“的意义并不只仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工做,更􏰀要的是在它进行垃圾收集时,必须暂停其余全部的工做线程,直到它收集结束(Stop the world)。

在这里插入图片描述

优势:实现简单高效(与其余收集器的单线程相比),对于限定单个CPU的环境来讲,Serial收集器因为没有线程交互的开销,专心作垃圾收集天然能够得到最高的单线程收集效率。

缺点:会给用户带来停顿。

适用场景:Client 模式(桌面应用);单核服务器。

能够用 -XX:+UseSerialGC 参数能够指定年轻代和老年代都使用串行收集器。等价于新生代用 Serial GC,而且老年代用 Serial Old GC。

import java.util.ArrayList; 
import java.util.List;
/** * -XX:+UseSerialGC -XX:+PrintCommandLineFlags */
public class GCUserTest {
    public static void main(String[] args){
        List<String> list = new ArrayList<>(); 
        String str = "hey";
        while(true){
            list.add(str); 
            str += str; try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace(); 
            }
        }
    }
}
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC

总结

这种垃圾收集器了解便可,如今已经不用串行的了。并且在限定单核CPU才能够使用,如今都不是单核的了。

对于交互较强的应用而言,这种垃圾收集器是不能接受的。通常在java web应用程序中是不会使用串行垃圾收集器的。

4.7 Serial Old(标记-整理算法)

老年代单线程收集器,Serial收集器的老年代版本;Serial Old是运行在Client模式下默认的老年代的垃圾回收器。Serial Old在server模式下主要有两个用途:

  1. 与新生代的Parallel Scavenge配合使用 ;

2)做为老年代CMS收集器的后备垃圾收集方法。

以下图是 Serial 收集器和 Serial Old 收集器结合进行垃圾收集的示意图:

在这里插入图片描述

4.8 ParNew (复制算法)

若是说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器就是Serial收集器的多线程版本。

Par是Parallel的缩写,New:只能处理的是新生代

新生代收并行集器,ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。在多核CPU环境下有着比Serial更好的表现;ParNew收集器在年轻代中一样也是采用复制算法、“stop- the-wold”机制。

以下是 ParNew 收集器和 Serial Old 收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,全部线程暂停执行,ParNew 收集器以多线程,采用复制算法进行垃圾收集工做,收集完以后, 用户线程继续开始执行。

在这里插入图片描述

  • 对于新生代,回收次数频繁,使用并行方式高效。
  • 对于老年代,回收次数少,使用串行方式节省资源。(CPU并行须要切换线程,串行能够省去切换线程的资源)。

因为ParNew收集器是基于并行回收,那么是否能够判定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?(扩展点:多线程程序效率必定高于单线程程序吗??)

ParNew收集器运行在多CPU的环境下,因为能够充分利用多CPU、多核心等物理硬件资源优点,能够更快速地完成垃圾收集,提高程序的吞吐􏰂量。

可是在单个CPU的环境下,ParNew收集器不比Serial收集器更高效。虽然Serial收集器是基于串行回收,可是因为CPU不须要频繁地作任务切换,所以能够有效避免多线程交互过程当中产生的一些额外开销。

适用场景

多核服务器;与 CMS 收集器搭配使用(除Serial外,目前只有ParNew GC能与CMS收集器配合工做)。

参数

​ 当使用 -XX:+UseConcMarkSweepGC 来选择 CMS 做为老年代收集器时,新生代收集器默认就是 ParNew,也能够用 -XX:+UseParNewGC 来指定使用 ParNew 做为新生代收集器。

​ -XX:ParallelGCThreads 限制线程数􏰂,默认开启和cpu数据相同的线程数。

4.9 Parallel Scavenge(复制算法)

​ HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的之外,Parallel Scavenge收集器一样也采用了复制算法、并行回收和“stop the world”机制。

​ 那么Parallel收集器的出现是不是画蛇添足?

  • ​ 和ParNew收集器不一样,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量 (Throughput),它也被称为吞吐量优先的垃圾收集器。
  • ​ 自适应调节策略也是Parallel Scavenge与ParNew一个重􏰀要区别。

​ 高吞吐量􏰂意味着高效利用 CPU。高吞吐􏰂量能够高效率的利用CPU时间,尽快完成程序的运算任务。

​ 以下是 Parallel 收集器和 Parallel Old 收集器结合进行垃圾收集的示意图,在新生代,当用户线程都执行到安全点时,全部线程暂停执行,ParNew 收集器以多线程,采用复制算法进行垃圾收集工做, 收集完以后,用户线程继续开始执行;在老年代,当用户线程都执行到安全点时,全部线程暂停执行, Parallel Old 收集器以多线程,采用标记-整理算法进行垃圾收集工做。

在这里插入图片描述

适用场景:

​ 注􏰀重吞吐量􏰂,高效利用 CPU,须要高效运算且不须要太多交互。适合后台应用等对交互相应要求不高的场景;例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

参数配置:

-XX:+UseParallelGC 来选择 Parallel Scavenge 做为新生代收集器,

-XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。

  • 分别适用于新生代和老年代。默认jdk8是开启的。
  • 上面两个参数,默认开启一个,另外一个也会被开启。(互相激活)

4.10 Parallel Old (标记-整理算法)

​ 老年代并行收集器,吞吐量􏰂优先,Parallel Scavenge收集器的老年代版本;

在这里插入图片描述

​ 适用场景:与Parallel Scavenge收集器搭配使用;注􏰀重吞吐量􏰂。jdk七、jdk8 默认使用该收集器做为老年代收集器,使用 -XX:+UseParallelOldGC 来指定使用 Paralle Old 收集器。

4.11 CMS(Concurrent Mark Sweep)收集器(标记-清除 算法)

​ 在JDK1.5时,HotSpot推出了一款在强交互应用中􏰀要的一款垃圾收集器: CMS(Concurrent- Mark-Sweep),这款垃圾收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了 垃圾收集线程与用户线程同时工做

​ CMS收集器是一种尽量缩短用户线程的停顿时间**(低延迟)**收集器,停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提高用户体验。

​ 目前很大一部分的java应用集中在B/S系统的服务端上,这类应用尤为􏰀视服务的响应速度,但愿系统停顿时间最短,以给用户带来较好的体验。CMS收集器就很是符合这类应用的需求。

​ 它是一种并发收集器,采用的是标记-清除算法

JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。在G1出现以前,CMS使用仍是很是普遍的。一直到今天,仍然有不少系统使用CMS GC。

4.11.1 收集过程

整个垃圾收集过程分为 4 个步骤:

  1. 初始标记(Initial-Mark):在这个阶段中,程序中全部的用户线程都会由于STW机制而出现短暂的暂停,主要任务是标记一下 GC Roots直接关联到的对象,一旦标记完成就会恢复以前的用户线程,因为直接关联对象比较小,因此速度很是快。
  2. 并发标记(Concurrent-Mark):从 GC Roots 的直接关联对象开始遍历整个对象图的过程,标记出所有的垃圾对象,耗时较⻓。这个过程耗时较⻓可是不须要暂停用户线程,能够与垃圾收集线程一块儿并发执行。
  3. 新标记(Remark):因为在并发标记阶段中,程序的用户线程和垃圾收集线程同时运行或者交叉运行,所以为了修正并发标记期间,因用户线程继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记极端稍⻓一些,但也远比并发标记阶段的时间短。
  4. 并发清除(Current-Sweep):此阶段清理删除标记阶段判断的已经死亡的对象,释放内存空间。 因为不须要移动存活对象,因此这个阶段也是能够与用户线程同时并发的。

​ 整个过程耗时最⻓的并发标记和并发清除都是和用户线程一块儿工做,因此从整体上来讲,CMS 收集器垃圾收集能够看作是和用户线程并发执行的。

在这里插入图片描述

​ 尽管CMS收集器采用的是并发回收(非独占式),可是在其初始化标记和􏰀新标记这两个阶段中仍然须要执行"stop-the-world"机制暂停程序中的工做线程,不过暂停时间并不太⻓,所以能够说明目前所 有的垃圾收集器都作不到彻底不须要"stop-the-world",只是尽量地缩短暂停时间。

因为最耗费时间的并发标记与并发清除阶段都不须要暂停工做,因此总体的回收是低停顿的。

​ 另外,因为在垃圾收集阶段用户线程没有中断,因此在CMS回收过程当中,还应该确保应用程序用户线程有足够的内存可用。所以,CMS收集器不能像其余收集器那样等到老年代集合彻底被填满了再进行收集,而是当堆内存使用率达到某一阀值时,便开始进行回收。以确保应用程序在CMS工做过程当中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存没法知足程序须要,就会出现一 次"Conrurrent Mode Failure"失败,这时虚拟机将启动后备预案:临时启动Serial Old****收集器来􏰀新进 行老年的垃圾收集,这样停顿时间就很⻓了。

4.11.2 空闲列表

​ CMS收集器的垃圾收集算法采用是标记-清除算法,这意味这每次执行完内存回收后,因为被执行内存回收的无用对象所占用的内存空间极有多是不连续的一些内存块,不可能避免的讲会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将没法使用指针碰撞(Bump the Pointer)技术,而只可以选择空闲列表(Free List)执行内存分配。

在这里插入图片描述

​ 为对象分配空间的任务等同于把一块肯定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,全部用过的内存都放在一边,空闲的内存放在另外一边,中间放着一个指针做为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为**“指针碰撞”(Bump the Pointer)。若是Java堆中的内存并非规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”**(FreeList)。选择哪一种分配方式由Java堆是否规整决定,而Java堆是否规整 又由所采用的垃圾收集器是否带有压缩整理功能决定。

​ 所以,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞, 而使用CMS这种基于Mark-Sweep算法的收集器时,一般采用空闲列表。

指针碰撞:

在这里插入图片描述

空闲列表:

在这里插入图片描述

有人会以为既然Mark Sweep会形成内存碎片,那么为何不把算法换成Mark Compact?

答案其实很简单,由于当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用?要保证用户线程能继续执行,前提是它容许的资源不受影响。

4.11.3 主要优缺点

CMS主要优势:

1.并发收集;

2.低停顿。

CMS明显的缺点:

  1. **CMS收集器对CPU资源很是敏感。**在并发阶段,它虽然不会致使用户线程停顿,可是会由于占用了一部分线程而致使应用程序变慢,总吞吐量􏰂会下降。

  2. CMS收集器没法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而致使另外一次Full GC 的产生。因为CMS并发清理阶段用户线程还在运行着,伴随程序运行天然就还会有新的垃圾不断产生, 这部分垃圾出如今标记过程以后,CMS没法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

  3. CMS是基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多, 可能会出现老年代还有很大空间剩余,可是没法找到足够大的连续空间来分配当前对象,不得不提早触发FullGC。

4.11.4 经常使用参数设置

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  1. -XX:+UseConcMarkSweepGC 手动执行使用CMS收集器执行内存回收任务。开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区) + CMS(Old区) + Serial Old的组合

2)-XX:CMSInitiatingOccupancyFraction 设置堆内存使用率的阀值,一旦达到该阀值,便开始进行回收。

-XX:CMSInitiatingOccupancyFraction=20 设置到20%时

若是内存增⻓缓慢,则能够设置一个稍大的值,大的阀值能够有效下降CMS的触发频率,检索老年代回收的次数能够较为明显的改善应用程序性能。

相反,若是应用程序内存使用率增⻓很快,则应该下降这个阀值,以免频繁触发老年代串行收集器。经过该选项能够有效下降Full GC的执行次数。

4.11.5 小结

HotSpot这么多的垃圾回收器,Serial/Serial Old、Parallel GC、CMS这些GC有什么不一样吗?

  • 若是你想要最小化地使用内存和并行开销,请选择Serial Old(老年代) + Serial(年轻代)
  • 若是你想要最大化应用程序的吞吐􏰂,请选择Parallel Old(老年代) + Parallel(年轻代)
  • 若是你想要最小化GC的中断或停顿时间,请选择CMS(老年代) + ParNew(年轻代)

后续版本

JDK9新特性:CMS被标记**废弃(Deprecate)**了,若是对JDK9 以以上版本的Hotspot虚拟机使用- XX:+UseConcMarkSweepGC参数来开启CMS收集器的话,用户会收到一个警告信息,同时CMS在将来将会被废弃

JDK14新特性:删除CMS垃圾回收器,移除CMS垃圾收集器,若是在JDK14中使用- XX:+UseConcMarkSweepGC的话,JVM不会报错,只是给出一个warning信息,可是不会exit,JVM会自动回退以默认GC的方式启动JVM。

4.12 G1(Garbage First)收集器 (区域化分代式)

既然已经有了前面的几个强大的GC,为何还要发布Garbage First(G1)GC ?

​ 缘由在于应用程序所对应的业务愈来愈庞大、复杂、用户愈来愈多,没有GC就不能保证应用程序正常运行,而常常形成STW的GC又跟不上实际的需求,因此才会不断地尝试对GC进行优化。G1垃圾回收器是在java7 update4以后引入的一个新的垃圾回收器,是当前收集器技术发展的最前沿成果之一。 G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不一样于以前的收集器的一个􏰀要特色是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

​ 与此同时,为了适应如今不断扩大的内存和不断增长的处理器数量,进一步下降暂停时间(pause time),同时兼顾良好的吞吐量

​ 官方给G1设定的目标是在延迟可控的状况下得到尽量高的吞吐量,因此才担当起**“全功能收集器”**的􏰀任与指望。

​ G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。所以它是一款并行与并发收集器,而且它能创建可预测的停顿时间模型。

4.12.1 为何叫作**Garbage First(G1)**呢?

由于G1是一个并行回收器,它用堆内存分割为不少不相关的区域(Region)(物理上是不连续的)。 使用不一样的Region来表示Eden、survivor、old等。

在这里插入图片描述

​ G1 GC有计划的避免在整个java堆中进行全区域的垃圾收集。G1 跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),**在后台维护一个优先列表,每次根据 容许的收集时间,优先回收价值最大的Region

​ 因为这种方式的侧􏰀点在于回收垃圾最大􏰂的区间(Region),因此咱们给G1一个名字:垃圾优先 (Garbage First)

​ G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容􏰂内存的机器,以极大几率知足GC停顿时间的同时,还兼具高吞吐量的性能特性。

​ 在JDK1.7版本正式启用,移除了Experimetal的标识,是JDK9之后的默认垃圾回收器,取代了CMS 回收器以及Parallel + ParallelOld 组合。被Oracle官方称为**“全功能的垃圾收集器”**。

​ 与此同时,CMS已经在JDK9中被标记为废弃(deprecated)。在jdk8中还不是默认的垃圾回收器,须要使用-XX:+UseG1GC来启用。

4.12.2 G1收集器的优势

与其余GC收集器相比,G1使用全新的分区算法,其特色以下所示:

1) 并行与并发

  • 并行性: G1在回收期间,能够有多个GC线程同时工做,有效利用多核计算能力。此时用户线程 STW。
  • 并发性:G1拥有与用户线程交替执行的能力,部分工做能够和应用程序同时执行。所以,通常来讲,不会再整个回收阶段发生彻底阻塞应用程序的状况。

2)分代收集

从分代上看,G1依然属于分代行垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和 Survivor区。但从堆的结构上,它不要求整个Eden区、Survivor区或者老年代都是联系的,也再也不坚持 固定大小和固定数􏰂。

将堆空间划分为若干个区域**(Region)**,这些区域包含了逻辑上的年轻代和老年代。

和以前的各种垃圾回收器不一样,它同时兼顾年轻代和老年代。对比其余回收器,或者工做在年轻代,或者工做在老年代。

在这里插入图片描述

在这里插入图片描述

3) 可预测的停顿时间模型(即:软实时 soft real time)

G1会经过一个合理的计算模型,计算出每一个Region的收集成本并􏰂化,这样一来,收集器在给定了“停顿”时间限制的状况下,老是能选择一组恰当的Regions做为收集目标,让其收集开销知足这个限制 条件,以此达到实时收集的目的。G1收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个java堆中进行全区域的垃圾收集。

  • 因为分区缘由,G1能够只选取部分区域进行内存回收,这样缩小了回收的范围,所以对于全局停顿的发生也能获得较好的控制。
  • G1跟踪各个Reion里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需的时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的Region。保证了G1收集器在优先的时间内能够获取尽量高的收集效率。
  • 相比于CMS GC,G1未必能作到CMS在最好状况下的延时停顿,可是最差状况要好不少。

如何创建可靠的停顿预测模型(知足用户设定的指望停顿时间)?

G1 收集器的停顿模型是以衰减均值(Decaying Average)为理论基础来实现的:垃圾收集过程 中,G1收集器会根据每一个 Region 的回收耗时、记忆集中的脏卡数量􏰂等,分析得出平均值、标准误差等。

“衰减平均值”比普通的平均值更能准确地表明“最近的”平均状态,经过这些信息预测如今开始回收的话,由哪些 Region 组成回收集才能在不超指望停顿时间的约束下得到最高收益。

4.12.3 G1收集器的缺点

相对于CMS,G1还不具有全方位、压倒性优点。好比在用户程序运行过程当中,G1不管是为垃圾收集产生的内存占用仍是程序运行时的额外执行负载都要比CMS要高。 从经验上来讲,总体而言:

  • 小内存应用上,CMS 大几率会优于 G1;
  • 大内存应用上,G1 则极可能更胜一筹。

这个临界点大概是在 6~8G 之间(经验值)

4.12.4 G1回收器的参数设置

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  • -XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务。
  • ‐XX:G1HeapRegionSize 设置每一个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的java堆大小划分出约2048个区域。默认是堆内存的1/2000。
  • ‐XX:MaxGCPauseMillis 设置指望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认200ms。

4.12.5 如何设置

G1的设计原则就是简化JVM性能调优,只须要简单三步便可完成:

  • 第一步: 开启G1垃圾收集器 (-XX:+UseG1GC)
  • 第二步:设置堆的最大内存( -Xmx -Xms)
  • 第三步:设置最大的停顿时间(‐XX:MaxGCPauseMillis)

4.12.6 收集过程

以下图所示,G1 收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和 CMS 收集器前几步的收集过程很类似:

  1. 初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,须要中止用户线程,单线程执行。
  2. 并发标记:从 GC Root 开始对堆中的对象进行可达性分析,找出存活对象,这个阶段耗时较⻓,但能够和用户线程并发执行。
  3. 最终标记:修正在并发标记阶段因为用户程序执行而产生变更的标记记录。
  4. 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所指望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First 的由来 ——第一时间清理垃圾最多的区块),这里为了提升回收效率,并无采用和用户线程并发执行的方式,而是停顿用户线程。

在这里插入图片描述

4.12.7 G1回收器的使用场景

  1. 面向服务端应用,针对具备大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)

  2. 最主要的应用是须要低GC延迟,并具备大堆的应用程序提供解决方案;

3)在堆大小约6GB或更大时,可预测的暂停时间能够低于0.5秒;(G1经过每次只清理一部分而不是所有 Region的增􏰂式清理在保证每次GC停顿时间不会过⻓) 。

4)用来替换掉JDK1.5中的CMS收集器,如下状况,使用G1可能比CMS好

  • 超过50% 的java堆被活动数据占用;
  • 对象分配频率或年代提高频率变化很大;
  • GC停顿时间过⻓(大于0.5至1秒)
  1. HotSpot垃圾收集器里,除了G1之外,其余的垃圾收集器使用内置的JVM线程执行GC多线程操做, 而G1 GC能够采用应用线程运行GC的工做,即当JVM的GC线程处理速度慢时,系统会调用应用程序帮助加速垃圾回收过程。

4.12.8 Region的使用介绍

分区Region:化整为零

​ 使用G1收集器是,它将整个java堆划分为约2048个大小相同的独立Region块,每一个Region块大小 根据堆空间的实际大小而定,总体被控制在1mb到32mb之间,且为2的N次幂,即1mb、2mb、 4mb、8mb、16mb、32mb。能够经过‐XX:G1HeapRegionSize设定。全部的Region大小相同,且在 JVM生命周期内不会被改变。

虽然还保留这新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分 Region(不须要连续)的集合。经过Region的动态分配方式实现逻辑上的连续。

在这里插入图片描述

​ 一个region有可能属于Eden,Survivor,或者Old 内存区域。可是一个region只可能属于一个⻆ 色。图中的E表示region属于Eden内存区域,S表示属于Survivor内存区域,O表示属于Old内存区域。 图中空白的表示未使用的内存空间。

​ G1垃圾收集器还增长了一种新的内存区域,叫作Humongous内存区域,如图中的H块。主要用于 存储大对象,若是超过1.5个region,就放到H区。

设置H的缘由:

​ 对于堆中的大对象,默认直接会分配到老年代,可是若是他是一个短时间存在的大对象,就会对垃圾收集器形成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专⻔存放大对象。**若是一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。**为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区做为老年代一部分来看待。

4.12.9 主要回收环节

G1 GC的垃圾回收过程主要包含如下三个环节:

  • 年轻代GC (Young GC)
  • 老年代并发标记过程(Concurrent Marking)
  • 混合回收(Mixed GC)**

(若是须要,单线程、独占式、高强度的Full GC仍是继续存在的。Full GC针对GC 的评估失败提供了一 种失败的保护机制,即强力回收。)

在这里插入图片描述

Young GC -> Young GC + concurrent mark -> mixed GC 顺序,进行垃圾回收。

年轻代GC

​ 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停全部的应用程序线程,启动多线程执行年轻代回 收。而后从年轻代区间移动存活对象到Survivor区间或者老年代区间,也有多是两个区间都会涉及。

老年代并发标记(Concurrent Marking)

​ 当堆内存使用达到必定值(默认是45%)时,开始老年代并发标记过程。

混合回收(Mixed GC)

​ 标记完成⻢上开始混合回收过程。对于一个混合回收期,G1 GC从老年代移动存活对象到空闲区 间,这些空闲区间也就成为了老年代的一部分。和年轻代不一样,老年代的G1回收器和其余GC不一样,G1 的老年代回收器不须要整个老年代被回收,一次主要扫描/回收一小部分老年代Region就能够了。同时,这个老年代Region是和年轻代一块儿被回收的。

​ 举个示例:一个Web服务器,java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新 分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%。会 开始老年代并发标记过程,标记完成后开始四到五次的混合回收。

4.12.10 G1回收器优化建议

1)年轻代大小

  • 固定年轻代的大小会覆盖暂停时间目标
  • 避免使用-Xmn或者-XX:NewRatio等相关选项显示设置年轻代大小

2)暂停时间目标不要太过严苛

  • 评估G1 GC的吞吐􏰂时,暂停时间目标不要太严苛。若是太严苛表示你愿意承受更多的垃圾回收开销,而这样会直接影响吞吐量􏰂
  • G1 GC的吞吐􏰂目标是90%的应用程序时间和10%的垃圾回收时间

​ 从Oracle官方透露出来的信息可知,回收阶段(Evacuation)其实本也有想过设计成与用户一块儿并发执行,但这件事情作起来比较复杂,考虑到G1只是回收一部分Region,停顿时间是用户可控制的,因此并不迫切去实现,而选择把这个特性放到了G1以后出现的低延迟垃圾收集器(即ZGC)中。另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程可以最大幅度提升垃圾收集效率,为了保证吞吐􏰂因此才选择了彻底暂停用户线程的实现方案。

4.13 垃圾回收器总结

GC 发展阶段

Serial => Parallel(并行) => CMS(并发) => G1 => ZGC

​ 截止jdk1.8 ,一共有7款不一样垃圾收集器。每一款不一样的垃圾收集器都有不一样的特色,在具体使用 的时候,须要根据具体的状况选择不一样的垃圾回收器

在这里插入图片描述

4.14 ZGC

官方文档:https://docs.oracle.com/en/java/javase/12/gctuning/

​ ZGC: A Scalable Low-Latency Garbage Collector (Experimental)(ZGC: 可伸缩的低延迟垃圾回收器,处于实验性阶段) http://openjdk.java.net/jeps/333

​ ZGC的目标是:在尽量对吞吐量影响不大的前提下,实现任意堆内存大小下均可以把垃圾收集的停 顿时间限制控制在十毫秒之内的低延迟。

​ 《深刻理解java虚拟机》一书中这样定义ZGC:ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针的内存多􏰀映射等技术来实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器。

​ ZGC的工做过程能够分为4个阶段:并发标记-并发预备重分配-并发重分配-并发重映射等。

​ ZGC几乎在全部地方都是并发执行的,除了初始标记是STW的。全部停顿时间几乎就耗费在初始标记上,这部分的实际时间是很是少的。

​ 虽然ZGC还在试验阶段,没有完成全部特性,但此时性能已经至关亮眼,用**“使人震惊、革命性”**来形容,都不为过。

​ 将来将在服务端、大内存、低延迟应用的场景下首选垃圾收集器。

​ JDK14以前,ZGC仅在Linux才支持。

​ 尽管许多使用ZGC的用户都使用类Linux的环境,但在Windows和macOS上,人们也须要ZGC进行开发部署和测试。许多桌面应用也能够从ZGC中受益。所以,ZGC特性被移植到了Windows和macOS 上。

如今mac或Windows上也能使用ZGC了,参数配置以下:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

后续内容请看JVM垃圾回收机制 关注做者不迷路,持续更新高质量Java内容~ 原创不易,您的支持/转发/点赞/评论是我更新的最大动力!