【从零开始学JVM】第四章_垃圾回收

发布于:2024-02-27 ⋅ 阅读:(61) ⋅ 点赞:(0)

文章目录

第三章_垃圾回收

1.介绍

1.1面试题

1.1.1经典面试题

关于垃圾收集有三个经典问题:

  • 哪些内存需要回收?what
  • 什么时候回收?when
  • 如何回收?how

1.1.2大厂面试题

  1. 蚂蚁金服

    1. 你知道哪几种垃圾回收器,各自的优缺点,重点讲一下cms和G1?
    2. JVM GC算法有哪些,目前的JDK版本采用什么回收算法?
    3. G1回收器讲下回收过程GC是什么?为什么要有GC?
    4. GC的两种判定方法?CMS收集器与G1收集器的特点
  2. 百度

    1. 说一下GC算法,分代回收说下
    2. 垃圾收集策略和算法
  3. 天猫

    1. JVM GC原理,JVM怎么回收内存
    2. CMS特点,垃圾回收算法有哪些?各自的优缺点,他们共同的缺点是什么?
  4. 滴滴

    1. Java的垃圾回收器都有哪些,说下g1的应用场景,平时你是如何搭配使用垃圾回收器的
  5. 京东

    1. 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。垃圾回收算法的实现原理

阿里
讲一讲垃圾回收算法。
什么情况下触发垃圾回收?
如何选择合适的垃圾收集算法?
JVM有哪三种垃圾回收器?

字节跳动
常见的垃圾回收器算法有哪些,各有什么优劣?
System.gc()和Runtime.gc()会做什么事情?
Java GC机制?GC Roots有哪些?
Java对象的回收方式,回收算法。
CMS和G1了解么,CMS解决什么问题,说一下回收的过程。
CMS回收停顿了几次,为什么要停顿两次?

1.1.3面试题

(1)GC是什么?为什么要有GC?

GC是垃圾收集的意思,内存处理是开发人员容易出现问题的地方,忘记或者错误地内存回收会导致程序或者系统的不稳定甚至崩溃,Java提供的垃圾回收机制可以自动检测对象是否超过作用域从而达到自动回收的目的。

(2)简述Java垃圾回收机制

在Java开发中,程序员并不需要显式去释放一个对象的内存的,而是由虚拟机自动进行管理。在JVM中,有一个低优先级的垃圾回收线程,在正常情况下这个线程是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的对象集合中,然后进行回收操作

(3)如何判断一个对象是否存活?

判断一个对象是否存活有两种方法:引用计数法和可达性分析法
以下将对两种方法展开介绍:

1.1什么是垃圾

在这里插入图片描述

  1. 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
  2. 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。

1.2GC的作用

  1. 对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。

  2. 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象

  3. 随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。

1.3早期的垃圾回收

在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。比如以下代码:

MibBridge *pBridge= new cmBaseGroupBridge();
//如果注册失败,使用Delete释放该对象所占内存区域
if (pBridge->Register(kDestroy) != NO ERROR)
	delete pBridge;

这种方式可以灵活控制内存释放的时间,但是会给开发人员带来**频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。**

在有了垃圾回收机制后,上述代码极有可能变成这样

MibBridge *pBridge = new cmBaseGroupBridge(); 
pBridge->Register(kDestroy);

现在,除了Java以外,C#、Python、Ruby等语言都使用了自动垃圾回收的思想,也是未来发展趋势,可以说这种自动化的内存分配和垃圾回收方式已经成为了现代开发语言必备的标准。

1.4垃圾回收机制

  • 自动内存管理,无需开发人员手动参与内存的分配与回收,这样==降低内存泄漏和内存溢出的风险==

    没有垃圾回收器,java也会和cpp一样,各种悬垂指针,野指针,泄露问题让你头疼不已。

  • 自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发

oracle官网关于垃圾回收的介绍:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html

1.5担忧

对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会==弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。==

此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见outofMemoryError时,快速地根据错误异常日志定位问题和解决问题。

当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节

1.6GC主要关注的区域

在这里插入图片描述

垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收。其中,Java堆是垃圾收集器的工作重点

JVM中堆可分为:

  • 新生代
    • Eden区域
    • From(s0)区域
    • To(s1)区域
  • 老年代

image-20240218193449464

JVM垃圾回收的主要区域之一就是我们的堆。JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

按照回收的区域,GC的分类:

  • 普通GC(minor GC)
  • 全局GC(major GC or Full GC)

Minor GC和Full GC的区别

  • 普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
  • 全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的),Major GC的速度一般要比Minor GC慢上10倍以上 。

2.垃圾回收相关算法

垃圾回收相关算法可以分为两个阶段:垃圾标记阶段和垃圾清除阶段,除此之外,简略介绍对象终止机制

  • 垃圾标记阶段
    • 引用计数算法
    • 可达性分析算法(根搜索算法、追踪性垃圾收集)
  • 垃圾清除阶段
    • 标记清除算法
    • 复制算法
    • 标记压缩算法
    • 分代收集算法
  • 对象终止机制

2.1垃圾标记阶段

2.1.1引用计数算法

对每个对象保存一个整形的引用计数器属性。用于==记录对象被引用的情况==。有引用+1.引用失效-1.当为0的时候对象就不再被使用,可进行回收

  • 优点:实现简单,垃圾对象便于辨识。判定效率高,回收没有延迟性。(发现引用次数为0后直接删除,而不用等待一起GC)

  • 缺点:

    1. 它需要单独的字段存储计数器,会增加存储空间开销。

    2. 每次复制都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。

    3. 引用计数器有一个严重的问题,即无法处理循环引用的情况。因此Java的垃圾回收器中没有使用这类算法(内存泄露)

      代码举例如下:

      class A {
          B b;
      
          public void setB(B b) {
              this.b = b;
          }
      }
      
      class B {
          A a;
      
          public void setA(A a) {
              this.a = a;
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              A a = new A();
              B b = new B();
              a.setB(b);
              b.setA(a);
      
              // 现在a和b相互引用,它们的引用计数都为1
      		
              // 当不再使用a和b时,它们的引用计数仍然为1,
              // 因为它们之间的循环引用导致引用计数无法归零
              
              //需要设置后a.setB(null)以及b.setA(null),才可以被垃圾回收所回收
          }
      }
      

2.1.2可达性分析算法(根搜索算法、追踪性垃圾收集)

可达性分析算法是一种常用的垃圾回收算法,它通过根对象(如栈中的引用、静态变量等)出发,标记所有可达的对象,未被标记的对象即为不可达对象,可以被回收。

可以作为GC Roots的对象有以下几种:

  1. 虚拟机栈中引用的对象
  2. 方法区类静态属性引用的对象
  3. 方法区常量池引用的对象
  4. 方法栈JNI引用的对象

这种算法可以正确地处理循环引用的情况,并且能够准确地确定对象的生命周期。可达性分析算法有许多不同的实现方式,下面分别介绍根搜索算法和追踪性垃圾收集。

(1)根搜索算法

根搜索算法是可达性分析算法的一种实现方式,它从程序的根对象开始,递归地遍历所有根对象可达的对象,并标记这些对象,最终未被标记的对象即为不可达对象,可以被回收。

public class Main {
    public static void main(String[] args) {
        // 创建对象 a 和 b
        A a = new A();
        B b = new B();

        // 对象 a 引用对象 b
        a.setB(b);

        // 对象 b 引用对象 a
        b.setA(a);

        // 将对象 a 和 b 设置为根对象
        Root.add(a);
        Root.add(b);

        // 执行垃圾回收
        GarbageCollector.collect();

        // 未被标记的对象即为不可达对象,可以被回收
    }
}

在这个例子中,Root 类表示程序的根对象,GarbageCollector 类表示垃圾回收器。当执行 GarbageCollector.collect() 方法时,根搜索算法会从根对象 ab 开始,递归地遍历所有可达的对象,并标记这些对象,最终未被标记的对象即为不可达对象,可以被回收。

2.1.3GC Roots(根对象集合)

  • 根对象集合包括:
    1.虚拟机栈/本地方法栈中引用的对象(局部变量表)
    2.方法区/堆中静态属性引用的对象。(Java类的引用类型静态变量)
    3.方法区/堆中常量引用的对象(字符串常量池里的引用)
    4.被同步锁synchronized持有的对象
    5.java虚拟机内部的引用(基本数据类型对应的Class对象,一些常驻异常对象 (NullpointerException OutOfMemoryError)系统类加载器)
    6.反映java虚拟机内部情况的JMXXBean\JVMTI中注册的回调\本地代码缓存等
    优点:
    实现简单并且执行高效,并且可以有效地接再循环引用问题,防止内存泄露的发生。

2.1.4对象终止机制

2.2垃圾清除阶段

当区分出存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间。

2.2.1标记-清除(Mark-Sweep)算法

(1)介绍

标记-清除算法(Mark and Sweep Algorithm)是一种经典的垃圾回收算法,用于识别和回收不再使用的内存对象。该算法分为两个阶段:标记阶段和清除阶段。

(2)流程

下面是标记-清除算法的基本流程:

  1. 标记阶段

    • 从根对象开始,递归遍历所有可达的对象,并将它们标记为“存活”。

    • 所有未被标记的对象被认为是“垃圾”,即不可达对象。

  2. 清除阶段

    • 遍历整个堆内存,将未被标记的对象回收释放。

    • 清除后留下的空闲空间可以用于新的对象分配。

(3)优缺点

标记-清除算法主要优点是简单且直观,能够准确识别不可达对象并进行回收。然而,该算法也存在一些缺点:

  • 内存碎片:清除后可能会产生内存碎片,导致内存利用率降低。
  • 暂停时间长:在清除阶段,需要遍历整个堆内存来回收对象,可能会导致较长的暂停时间,影响程序的性能。
(4)示意图

img

2.2.2复制算法

(1)介绍

复制算法(Copying Algorithm)是一种垃圾回收算法,它将内存分为两个区域,每次只使用其中一个区域,当这个区域用完时,将还存活的对象复制到另一个区域中,然后清空原来的区域。通过这种方式,可以有效地回收不再使用的内存,同时避免内存碎片的产生。

复制算法通常用于实现新生代的垃圾回收器。在新生代中,对象的生命周期较短,大部分对象很快就会变成垃圾。因此,新生代的垃圾回收器需要频繁地执行垃圾回收,以尽快回收不再使用的内存。同时,由于新生代的内存空间通常比较小,使用复制算法可以更有效地利用可用的内存空间。

(2)流程

当使用复制算法进行垃圾回收时,堆内存通常会被划分为新生代(Young Generation)和老年代(Old Generation)。在新生代中,通常会划分为==From区、To区和Eden==区。

  1. Eden区:这是新对象被分配的起始区域。当一个新的Java对象被创建时,它们会被分配到Eden区。大多数对象在创建后很快就变得不可达,因此Eden区通常会积累大量的对象实例。
  2. From区:From区是新生代的一部分,用于存储当前活跃的对象。在进行垃圾回收之前,From区中存放着所有存活的对象。
  3. To区:To区也是新生代的一部分,用于在垃圾回收过程中存放复制过来的存活对象。在垃圾回收完成后,To区会成为新的From区。

基于上述区域的划分,复制算法在新生代中的具体执行流程如下:

  1. 初始状态:新对象被分配到Eden区。

  2. 垃圾回收触发:当Eden区满时,触发新生代的垃圾回收。

  3. 标记阶段从根对象开始,递归遍历所有可达的对象,并将其标记为“存活”

    这些存活对象包括Eden区中的存活对象,以及From区中的存活对象。

  4. 复制阶段:将存活对象从Eden区和From区复制到To区,并按照对象的存放顺序进行紧凑排列。

  5. 角色交换:将From区和To区的角色互换,使To区成为新的From区,From区成为新的To区。

  6. 清空原From区:清空原来的From区,将其作为可用内存空间。

  7. 继续分配新对象:新对象继续分配到Eden区。

(3)优缺点
  • 优点:

    • 简单高效:只需扫描存活对象,无需遍历整个堆内存。

    • 消除内存碎片:通过复制存活对象到另一个区域并紧凑排列,消除了内存碎片的问题。

  • 缺点:

    • 需要额外的空间:每次只能使用一半的内存空间,可能导致一部分空间浪费。
    • 对大对象的处理:对于大对象或长时间存活的对象,复制操作可能较为耗时。
(4)示意图

image-20240219193552653

2.2.3标记-压缩算法(Mark-Compact)

(1)介绍

标记-压缩算法(Mark-Compact Algorithm)是一种用于垃圾回收的内存管理算法,常用于老年代的垃圾回收器。与复制算法不同,标记-压缩算法在原有的内存空间上进行操作,通过标记和移动对象来回收垃圾并压缩内存。

(2)流程
  1. 标记阶段:从根对象开始,递归遍历所有可达的对象,并将其标记为“存活”。
  2. 压缩阶段:遍历整个堆内存,将存活的对象按顺序移动到一侧,同时更新对象引用的指针,使其指向新的位置。这样,所有存活对象都被紧凑地排列在一起,形成连续的内存块。
  3. 更新引用:对于那些指向移动后位置的对象引用,需要更新它们的指针,使其指向新的位置。
  4. 释放空间:将未被标记的对象所占用的内存空间释放出来,以供下次使用。
(3)优缺点
  • 优点

    • 消除内存碎片:通过将存活对象紧凑排列在一起,消除了内存碎片的问题,从而提高了内存利用率。
    • 消除复制算法当中,内存减半的高额代价!
  • 缺点:

    • 需要额外的时间:相对于复制算法,标记-压缩算法需要遍历整个堆内存并进行对象移动,因此在执行过程中可能会消耗更多的时间。
    • 需要空闲空间:为了进行对象的移动和压缩,算法需要一定的空闲空间,以便在移动对象时进行重叠。
(4)示意图

img

2.2.4分代收集算法

(1) 介绍

分代收集算法(Generational Garbage Collection)是一种常用的垃圾回收策略,主要基于两个假设:大部分对象很快就会变得不可达(短时间存活),而少部分对象会长时间存活(长时间存活)。根据这一假设,将==堆内存划分为不同的代==(Generation),并针对不同代的对象采用不同的垃圾回收算法。

它包括两个主要阶段:新生代收集和老年代收集

(2)流程

工作原理:

  • 新生代垃圾回收
    • 复制算法常用于新生代,将新生代划分为Eden区和两个Survivor区(From和To)。
    • 新创建的对象被分配到Eden区,经过一次垃圾回收后存活的对象会被移动到Survivor区,然后在From区和To区之间进行复制,最终被晋升到老年代。
    • 这种方式有效地处理了大部分对象很快变成垃圾的情况,并减少了复制算法的复杂度。
  • 老年代垃圾回收
    • 对于老年代,通常采用**标记-清除-整理(Mark-Sweep-Compact)标记-压缩(Mark-Compact)**算法。
    • 标记-清除算法用于标记存活对象并清除垃圾,但可能产生内存碎片;标记-压缩算法则在清除垃圾后将存活对象向一端移动,解决了内存碎片问题。
  • 持久代垃圾回收
    • 持久代主要用于存放类的元数据、静态变量等,通常使用标记-清除算法进行垃圾回收。
(3)优缺点
  1. 效率高:分代收集算法通过根据对象的生命周期将内存划分为不同的代,针对不同代采取不同的回收策略,因此可以提高垃圾回收的效率。
  2. 对新生代的优化:由于大多数对象的生命周期较短,新生代收集频率通常比老年代低,这有助于减少全局垃圾回收的开销。
  3. 提高程序性能:通过更有效地管理内存,分代收集算法可以减少程序因频繁的垃圾回收而产生的停顿时间,从而提高程序的整体性能。

缺点:

  1. 需要细致的调节:分代收集算法需要细致地调节各代的大小和回收策略,如果配置不当可能会导致性能下降。
  2. 内存占用较大:分代收集需要额外的空间来维护各代之间的引用关系和元数据,因此可能会导致整体内存占用较大。
  3. 复杂度高:分代收集算法相对于其他简单的垃圾回收算法来说,实现和调优的复杂度较高,需要更多的工作量和资源投入。

3.垃圾回收器

3.1概述

  1. 垃圾收集器**没有在规范中进行过多的规定**,可以由不同的厂商、不同版本的JVM]来实现。
  2. 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。

3.2分类

从不同角度分析垃圾收集器,可以将GC分为不同的类型。

3.2.1按线程分

线程数分,可以分为串行垃圾回收器并行垃圾回收器

img

串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。

  • 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中

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

和串行回收相反,并行垃圾收集器可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“Stop-the-World”机制。

3.2.2按工作模式分

按照工作模式分,可以分为并发式垃圾回收器独占式垃圾回收器

  • 并发式垃圾回收器与应用程序线程**交替工作**,以尽可能减少应用程序的停顿时间。
  • 独占式垃圾回收器(Stop the world)一旦运行,就**停止应用程序中的所有用户线程**,直到垃圾回收过程完全结束。

img

3.2.3按碎片处理方式分

碎片处理方式分,可分为压缩式垃圾回收器非压缩式垃圾回收器

  • 压缩式垃圾回收器会在回收完成后,对存活对象==进行压缩整理消除回收后的碎片==。
    • 再分配对象空间使用:指针碰撞
  • 非压缩式的垃圾回收器不进行这步操作。
    • 再分配对象空间使用:空闲列表

3.2.4按工作内存区间分

工作的内存区间分,又可分为年轻代垃圾回收器老年代垃圾回收器

3.3评估GC的性能指标

  1. 吞吐量运行用户代码的时间占总运行时间的比例

    (总运行时间 = 程序的运行时间 + 内存回收的时间)

  2. 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。

  3. 暂停时间执行垃圾收集时,程序的工作线程被暂停的时间

  4. 收集频率:相对于应用程序的执行,收集操作发生的频率。

  5. 内存占用:Java堆区所占的内存大小。

  6. 快速:一个对象从诞生到被回收所经历的时间。

吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。

这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。

简单来说,主要抓住两点:吞吐量、暂停时间

3.3.1吞吐量

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)。比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的

吞吐量优先,意味着在单位时间内,STW(独占式垃圾回收器:stop the world)的时间最短:0.2 + 0.2 = 0.4

在这里插入图片描述

3.3.2暂停时间

“暂停时间”是指一个时间段内应用程序线程暂停,让GC线程执行的状态。

例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。

暂停时间优先,意味着尽可能让单次STW的时间最短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5

在这里插入图片描述

3.3.3吞吐量vs暂停时间

高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快。

低暂停时间(低延迟)较好因为从最终用户的角度来看不管是GC还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型,**有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。**因此,具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序。

不幸的是”高吞吐量”和”低暂停时间”是一对相互竞争的目标(矛盾)。

  1. 因为如果选择以吞吐量优先,即减少内存回收的次数,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。(原因:如果内存回收的频率过高,那么应用程序将频繁地被暂停,导致系统的响应时间变长)
  2. 相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。

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

现在标准:在最大吞吐量优先的情况下,降低停顿时间

3.4垃圾回收器发展史

有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector。

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

3.5 7种经典的垃圾收集器

  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel old
  • 并发回收器:CMS、G1
7款经典收集器与垃圾分代之间的关系

在这里插入图片描述

  1. 新生代收集器:Serial、ParNew、Parallel Scavenge
  2. 老年代收集器:Serial Old、Parallel Old、CMS;
  3. 整堆收集器:G1;
垃圾收集器的组合关系

在这里插入图片描述

  1. 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  2. 其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。
  3. (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
  4. (绿色虚线)JDK14中:弃用Parallel Scavenge和Serialold GC组合(JEP366)
  5. (绿色虚框)JDK14中:删除CMS垃圾回收器(JEP363)(JDK9 Deprecated)

3.5.1有多种不同的垃圾收集器的原因?

为什么要有很多收集器,一个不够吗?因为Java的使用场景很多,移动端,服务器等。所以就需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。

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

如何查看默认垃圾收集器
-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)

使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID

3.5.2 Serial回收器:串行回收

(1)特点
  1. Serial收集器是最基本、历史最悠久的垃圾收集器了
  2. JDK1.3之前回收新生代唯一的选择。
  3. Serial收集器作为HotSpot中client模式下的==默认新生代垃圾收集器==。
  4. Serial收集器采用==复制算法串行回收"stop-the-World"==机制的方式执行内存回收。
  5. 除了年轻代之外,Serial收集器还提供用于==执行老年代垃圾收集的Serial Old收集器==。Serial Old收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。

在这里插入图片描述

  1. 这个收集器是一个 单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)
  2. 优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
(2)应用场景

对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。

在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。

(3)参数配置

在HotSpot虚拟机中,使用 -XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器

注意:在JDK Client模式,不指定VM参数,默认是串行垃圾回收器

  • 总结:
    • 新生代用Serial GC,且老年代用Serial Old GC
    • 这种垃圾收集器大家了解,现在已经不用串行的了。而且**在限定单核cpu才可以用**。现在都不是单核的了。
    • 对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java web应用程序中是不会采用串行垃圾收集器的。

3.5.3 Serial Old回收器

(1)特点
  1. Serial Old是Serial老年代版本
  2. 除了采用 标记整理算法,其他与Serial相同
(2)工作流程

img

如上图所示,Serial 收集器在新生代和老年代都有对应的版本,除了收集算法不同,两个版本并没有其他差异。

  • Serial 新生代收集器采用的是复制算法。
  • Serial Old 老年代采用的是标记 - 整理算法。
(3)应用场景
  1. Client模式
    Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
  2. Server模式
    如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;另一种用途就是作为CMS收集器的后备预案,在并发收集发生"Concurrent Mode Failure"时使用

3.5.4 ParNew回收器:并行回收

  1. 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。Par是Parallel的缩写,New:只能处理的是新生代
  2. ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。
  3. ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。

在这里插入图片描述

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

由于ParNew收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比serial收集器更高效?

(2)特点
  • ParNew 收集器运行 在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
  • 但是在单个CPU的环境下ParNew收集器不比Serial 收集器更高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。

因为除Serial外,目前只有ParNew GC能与CMS收集器配合工作

(3)参数配置
  1. -XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器

  2. "-XX:+UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。

  3. -XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。

(4)应用场景

ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。很重要的原因是:除了Serial收集器之外,目前只有它能与CMS收集器配合工作(看图)。在JDK1.5时期,HotSpot推出了一款几乎可以认为具有划时代意义的垃圾收集器-----CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

(5)只有ParNew能与CMS收集器配合使用的原因

CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;

因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

3.5.5 Parallel Scavenge回收器:吞吐量优先

(1)特点

HotSpot的年轻代中除了拥有ParNew收集器是基于==并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制==。

那么Parallel 收集器的出现是否多此一举?

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

高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序

Parallel 收集器在 JDK1.6 时提供了用于 执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器。

Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和**"Stop-the-World"机制**。

在这里插入图片描述

在程序吞吐量优先的应用场景中,Parallel 收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错。

在 Java 8 中,默认的垃圾收集器是并行垃圾收集器(Parallel Garbage Collector),它主要负责新生代的垃圾回收。对于新生代的内存回收,默认采用的是并行垃圾收集器,而对于 老年代 的内存回收,默认采用的是 串行垃圾收集器(Serial Garbage Collector)。

(2)参数配置
  • -XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。

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

    • 分别适用于新生代和老年代。默认jdk8是开启的。
    • 上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
  • -XX:ParallelGCThreads 设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。

    在这里插入图片描述

  • -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STw的时间)。单位是毫秒。

    • 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集在工作时会调整Java堆大小或者其他一些参数。
    • 对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
    • 该参数使用需谨慎。
  • -XX:GCTimeRatio 垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。

    • 取值范围(0, 100)。默认值99,也就是垃圾回收时间不超过1%。
    • 与前一个==-XX:MaxGCPauseMillis==参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
  • -XX:+UseAdaptivesizePolicy 设置Parallel Scavenge收集器具有自适应调节策略

    • 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
    • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

3.5.6Parallel Old收集器

(1)特点
  1. Parallel Old是Parallel Scavenge的老年代版本
  2. Parallel Old 老年代采用的是标记 - 整理算法,其他特点与Parallel Scavenge相同
  3. 和Parallel Scavenge搭配使用!!!
  4. img
(2)应用场景
  1. 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器组合。
  2. JDK1.6及之后用来代替老年代的Serial Old收集器;
  3. 特别是在Server模式,多CPU的情况下;

3.5.7 CMS回收器:低延迟

在JDK1.5时期,Hotspot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作

CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。

  • 目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
  • CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-World"

不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个

在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。

(1)CMS工作原理

在这里插入图片描述

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段

  1. 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
  2. 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  3. 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
  4. 并发清除(Concurrent-Sweep)阶段:此阶段**清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。**由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
(2)低延迟(理解)
  1. 尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“stop-the-World”,只是尽可能地缩短暂停时间。
  2. 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
  3. 另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
  4. CMS收集器的垃圾收集算法==采用的是标记清除算法==,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。

在这里插入图片描述

有人会觉得既然Mark Sweep(扫除)会造成内存碎片,那么为什么不把算法换成Mark Compact(压缩)

答案其实很简单,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢

要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Sweep更适合“Stop the World” 这种场景下使用

(3)优缺点

由于CMS采用的是标记-清除算法,所以CMS优点有并发收集、低延迟的特点,缺点则有产生内存碎片等缺点

  • 缺点还有:
    • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
    • CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
    • 如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样会导致另一次Full GC的产生。这样停顿时间就更长了,代价会更大,所以 "-XX:CMSInitiatingOccupancyFraction"不能设置得太大。
(4)设置的参数
  • -XX:+UseConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务。

    开启该参数后会自动将-xx:+UseParNewGC打开。即:ParNew(Young区用)+CMS(Old区用)+ Serial Old的组合

  • -XX:CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。‘

    • JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
    • 如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Ful1Gc的执行次数。
  • -XX:+UseCMSCompactAtFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。

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

  • -XX:ParallelcMSThreads 设置CMS的线程数量

    • CMS默认启动的线程数是(ParallelGCThreads+3)/4
    • ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
(5)小结

Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个垃圾回收器的区别?

  • 如果你想要最小化地使用内存和并行开销,请选Serial GC
  • 如果你想要最大化应用程序的吞吐量,请选Parallel GC
  • 如果你想要最小化GC的中断或停顿时间,请选CMS GC
(6)JDK8后续版本中CMS的变化
  • JDK9新特性:CMS被标记为Deprecate了(JEP291)

    如果对JDK9及以上版本的HotSpot虚拟机使用参数-XX: +UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。

  • JDK14新特性:删除CMS垃圾回收器(JEP363)

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

3.5.8 G1回收器:区域化分代式

(1)问题
  1. 既然我们已经有了前面几个强大的GC,为什么还要发布Garbage First(G1)

    原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。G1(Garbage-First)垃圾回收器是在Java7 update4之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。

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

官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。

  1. 为什么名字叫 Garbage First(G1)呢
    因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。

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

    由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。

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

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

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

(2)特点

与其他GC收集器相比,G1使用了全新的分区算法,其特点如下所示:

①并行与并发
  • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。
  • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
②分代收集
  1. 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。如下图所示
  2. 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代
  3. 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;

在这里插入图片描述

在这里插入图片描述

③空间整合
  1. CMS:“标记-清除”算法、内存碎片、若干次Gc后进行一次碎片整理

  2. G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。

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

这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

  1. 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
  2. G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,**每次根据允许的收集时间,优先回收价值最大的Region。**保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
  3. 相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
(3)缺点

相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。

从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

(4)参数配置
  1. -XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务
  2. -XX:G1HeapRegionSize: 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
  3. -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms(人的平均反应速度)
  4. -XX:+ParallelGCThread 设置STW工作线程数的值。最多设置为8(上面说过Parallel回收器的线程计算公式,当CPU_Count > 8时,ParallelGCThreads 也会大于8)
  5. -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
  6. -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
(5)常见操作步骤

G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

  • 第一步:开启G1垃圾收集器
  • 第二步:设置堆的最大内存
  • 第三步:设置最大的停顿时间

G1中提供了三种垃圾回收模式:Young GC、Mixed GC和Full GC,在不同的条件下被触发。

(6)适用场景

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

最主要的应用是需要==低GC延迟,并具有大堆的应用程序提供解决方案;如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式==清理来保证每次GC停顿时间不会过长)。

用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:

  • 超过50%的Java堆被活动数据占用;
  • 对象分配频率或年代提升频率变化很大;
  • GC停顿时间过长(长于0.5至1秒)

HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

(7)回收过程

1、初始标记(Initial Marking)

  • 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,
  • 速度很快,
  • 需要“Stop The World”。(OopMap)

2、并发标记(Concurrent Marking)

  • 进行GC Roots Tracing的过程,从刚才产生的集合中标记出存活对象;(也就是从GC Roots 开始对堆进行可达性分析,找出存活对象。)
  • 耗时较长,但应用程序也在运行;
  • 并不能保证可以标记出所有的存活对象;

3、最终标记(Final Marking)

  • 最终标记和CMS的重新标记阶段一样,也是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,
  • 这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,
  • 也需要“Stop The World”。(修正Remebered Set)

4、筛选回收(Live Data Counting and Evacuation)

  • 首先排序各个Region的回收价值和成本;
  • 然后根据用户期望的GC停顿时间来制定回收计划;
  • 最后按计划回收一些价值高的Region中垃圾对象;
  • 回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
  • 可以并发进行,降低停顿时间,并增加吞吐量;

3.6垃圾回收器总结

截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。

在这里插入图片描述

GC发展阶段:Serial => Parallel(并行)=> CMS(并发)=> G1 => ZGC

3.7垃圾回收器组合

不同厂商、不同版本的虚拟机实现差距比较大。HotSpot虚拟机在JDK7/8后所有收集器及组合如下图

在这里插入图片描述

  1. 两个收集器间有连线,表明它们可以搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
  2. 其中==Serial Old作为CMS出现"Concurrent Mode Failure"==失败的后备预案。
  3. (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial old这两个组合声明为Deprecated(JEP 173),并在JDK 9中完全取消了这些组合的支持(JEP214),即:移除。
  4. (绿色虚线)JDK 14中:弃用ParallelScavenge和SeriaOold GC组合(JEP 366)
  5. (绿色虚框)JDK 14中:删除CMS垃圾回收器(JEP 363) (JDK9 Deprecated)

3.8查看JDK的默认GC

3.8.1cmd命令行查看Java8的GC

java -XX:+PrintCommandLineFlags -version
  • 输出结果为:
-XX:InitialHeapSize=132397312 // JVM默认初始化堆大小
-XX:MaxHeapSize=2118356992 //JVM堆的默认最大值
-XX:+PrintCommandLineFlags 
-XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops 
-XX:-UseLargePagesIndividualAllocation 
-XX:+UseParallelGC //Java8使用的GC类型
java version "1.8.0_20" //使用的java版本
Java(TM) SE Runtime Environment (build 1.8.0_20-b26)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)

结果分析:由结果可以看出Java8的GC情况是:-XX:+UseParallelGC,即Parallel Scavenge(新生代) + Parallel Old(老生代),实际上几个主流Java版本的GC情况如下:

  • jdk1.7 默认垃圾收集器Parallel Scavenge(新生代【标记-复制算法】)+Parallel Old(老年代【标记整理算法】)
  • jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
  • jdk1.9 默认垃圾收集器G1【从局部(两个Region之间)来看是基于"标记—复制"算法实现,从整体来看是基于"标记-整理"算法实现】

4.总结面试题

  1. 并行和并发的区别

    • 并行指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态!!!如ParNew、Parallel等收集器

    img

    • 并发指用户线程与垃圾收集线程同时执行(注意:但不一定是并行的,可能会交替执行)

      用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如CMS、G1(G1也有并行)

  2. GC 是什么? 为什么要有 GC?

    GC就是垃圾回收,释放掉没用的对象占用的空间,保证内存空间不被迅速耗尽。

  3. 简单说一下java的垃圾回收机制。

    java采用分代回收,分为年轻代、老年代、永久代。年轻代又分为E区、S1区、S2区。

  4. 到jdk1.8,永久代被元空间取代了。

    年轻代都使用复制算法,老年代的收集算法看具体用什么收集器。默认是PS收集器,采用标记-整理算法。

  5. JVM的常见垃圾回收算法有哪些?

    复制、标记清除、标记整理、分代回收

  6. 为什么要使用分代回收机制?

    因为没有一种算法能适用所有场合。在对象存活率低的场景下,复制算法最合适

    对象存活率高时,标记清除或者标记整理算法最合适。所以才需要分代来处理。

  7. 如何判断一个对象是否存活?

    现在主流使用的都是可达性分析法。从GC Roots对象计算引用链,能链上的就是存活的。

  8. 如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?

    不会。对象回收需要一个过程,这个过程中对象还能复活。而且垃圾回收具有不确定性,指不定什么时候开始回收。

本文含有隐藏内容,请 开通VIP 后查看