Java垃圾回收—— 垃圾收集器

  • 概述
  • 垃圾收集器
  • 垃圾收集器组合
  • 概述

    在我们上一篇文章垃圾回收机制中讲述到了垃圾回收的三个要点 :

    1. 哪些内存需要回收?
    2. 什么时候回收?
    3. 如何回收?

    当然这些是内存回收的理论方法,那么今天要介绍的就是内存回收的具体实现。

    但是了解垃圾收集器之前,我们还需要了解如下四个知识点。


    Stop The World

    可达性分析对执行时间的敏感还体现在GC停顿上。

    因为这项分析工作必须在一个能确保一致性快照中进行——这里“一致性”的意思是在指整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。

    这点是导致GC进行时必须停顿所有Java执行线程的重要原因之一。

    换个通俗易懂的说话就是 : 判定垃圾回收的时候要保持整个引用不改变,(我在打扫房间,数地板上的垃圾时,不允许别人清理或者增加垃圾,否则就乱套了)。

    所以当可达性分析引用链的时候,就要全部暂停(STOP!),但是这个暂停时间特别短暂,对程序的影响也是微乎其微的,这就是GC卡顿的原因由来。


    枚举GC Roots

    从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量或者静态属性)与执行上下文(栈帧中的局部变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。

    上面提到 : 可达性分析对执行时间的敏感还体现在GC停顿上,也就是说枚举这些根节点的效率将会影响到GC的停顿时间。当然在虚拟机中还是有办法直接得知哪些地方存放着对象的引用。

    在HotSpot的实现中,使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成之后,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也只会在特定的位置(安全点)在记录下栈和寄存器中哪些位置是引用。


    安全点

    在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots枚举,只会在特定的位置记录了这些信息,这些位置称为安全点(Safepoint)。

    只有在安全点程序才能暂停下来开始GC,并且安全点的选定标准是 : 是否具有让程序长时间执行的特征为标准 。也就是说这个安全点不能因为指令长时间执行而导致停顿时间长。一般来说下面这三种情况的指令才会产生SafePoint。

    • 方法调用(临返回前/调用方法的call指令后)
    • 循环跳转(循环的末尾)
    • 异常跳转(可能抛出异常的位置)

    另外一个问题是 : 如何在GC发生时让所有的线程都“跑”到最近的安全点上再停顿下来?

    在JVM中采用的是主动式中断思想。

    主动式中断 : 当GC需要中断线程时,不直接对线程操作,简单的在安全点设置一个标志,各个线程执行到安全点的时候主动去轮询这个标志,发现中断标志为true就自己中断挂起。


    总结一下,安全点的主要作用是:

    • OopMap记录GC Roots信息。
    • 设置GC标志,当需要GC的时候,中断挂起所有执行的线程。


    安全区域

    但是如果线程处于Sleep或者Blocked状态时,无法响应JVM的中断请求。

    也就是说无法主动“跑”到安全点的位置并中断挂起。那么就需要安全区域(Safe Region)来处理。

    安全区域是指在一段代码之中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。

    所以只要线程执行到了Safe Region中的代码时做到以下两点即可 :

    1. 首先标识自己已经进入了Safe Region。
    2. 在线程要离开Safe Region时,检查系统是否完成了GC过程,如果完成了,那就继续执行。否则就等待到GC执行完毕的信号为止。

    ————————– 事不关己高高挂起,等你们处理完了我才来瞎掺和。 ————————–



    垃圾收集器

    了解垃圾收集器之前,我们先了解在垃圾回收器中以下几个名词

    吞吐量

    CPU用于运行用户代码的时间与CPU总消耗时间的比值。

    吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)。

    比如说虚拟机总运行了100分钟,用户代码时间99分钟,垃圾收集时间1分钟,那么吞吐量就是99%。


    GC的名词

    • 新生代GC(Minor GC) : 指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

    • 老年代GC(Major GC) : 指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(发生这种情况,那么整个堆都GC一遍,通常称为Full GC)。Major GC的速度一般会比Minor GC慢10倍以上。


    并行和并发

    • 串行 (Parallel) : 单线程垃圾收集工作,但此时用户线程仍然处于等待状态。

    • 并行 (Parallel) : 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。(与串行相比就是回收的执行过程中,多了几条垃圾收集线程并行工作)

    • 并发 (Concurrent) : 指用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上。


    这里写图片描述


    这里写图片描述





    了解完这些名词之后,我们开始了解7种不同的垃圾收集器。

    垃圾收集器是内存回收的具体实现,并且虚拟机会根据不同场景使用不同的垃圾回收器组合。

    常见的7种垃圾收集器有:

    • serial收集器、ParNew收集器(seria收集器的升级版,新生代)、serial Old收集器。

    • Parallel Scavenge收集器、Parallel Old 收集器。(吞吐量优先)

    • CMS(Concurrent Mark-Sweep)收集器。老年代。(停顿时间优先)

    • G1收集器(Garbage First)。整个堆。

    其中Serial收集器为串行收集器,其他均为并行收集器。


    Serial 收集器(单线程垃圾收集)(新,老)

    ——————– 最古老,最稳定,简单而高效,可能会产生较长的停顿。 ——————–

    Serial是一个单线程的收集器,它不仅仅只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。

    Serial垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。


    这里写图片描述

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

    • Serial 新生代收集器采用的是复制算法。
    • Serial Old 老年代采用的是标记 - 整理算法。


    ParNew 收集器(Serial 升级版)(新)

    ——————– Serial 新生代收集器升级版,多线程垃圾收集 ——————–

    ParNew收集器其实就是Serial新生代收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。

    在实现上,这两种收集器也共用了相当多的代码。ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。


    这里写图片描述



    Parallel 收集器(吞吐量优先)(新,老)

    ——————– 与ParNew收集器基本一样,但是更关注吞吐量 ——————–

    Parallel 收集器相对于其它收集器最大的特点和关注点就是达到一个可控制的吞吐量。

    上面也介绍过吞吐量这个名词,也就是垃圾收集收集对于总时间的占比。

    停顿时间与吞吐量 :

    停顿时间越短越适合需要用户交互的程序,良好的响应速度能提升用户体验

    吞吐量小则可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合后台运算而且不需要太多交互的任务。

    当然GC的停顿时间和吞吐量以及内存空间是有很大关联的 : GC更新频繁(吞吐量占比高),内存空间分配小(收集速度更快)。


    这里写图片描述

    如上图所示,Parallel 收集器在新生代和老年代也都有对应的版本,除了收集算法不同,两个版本并没有其他差异。
    并且Parallel 两个年代的收集器也可以说是 Serial 收集器的升级版,单线程(串行收集) – > 多线程(并行收集)。

    • Parallel Scavenge 新生代收集器采用的是复制算法。
    • Parallel Old 老年代采用的是标记 - 整理算法。

    在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器组合。



    CMS 收集器(停顿时间优先)(老)

    ——————– 最短回收停顿时间为目标的收集器,提供最好的用户体验 ,ART老年代默认收集器 ——————–

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

    CMS收集器是基于“标记 - 清除”算法实现的。也就是说在JVM中它是一款老年代垃圾收集器


    这里写图片描述

    CMS收集器运作过程相对比上面几种收集器来说更复杂一些,整个过程分为4个步骤 :

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

    • 并发标记(CMS concurrent mark)
      并发标记阶段就是进行GC Roots Tracing的过程。(从GC Roots 开始对堆进行可达性分析,找出存活对象。)

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

    • 并发清除(CMS concurrent sweep)
      并发清除阶段会清除对象。

    CMS是一款优秀的收集器,它的主要优点是 : 并发手机、低停顿。并且它还是ART老年代默认收集器

    但是CMS还达不到完美的程度,它有以下3个明显缺点 :

    1. CMS收集器对CPU资源非常敏感,因为在并发阶段它虽不会导致用户线程停顿,但是占用一部分线程(CPU资源),导致应用程序变慢,总吞吐量变低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。

    2. CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
      也是由于CMS垃圾并发收集阶段用户线程还需要运行,伴随着程序运行自然还会有新的垃圾不断产生,这一部分的垃圾出现在标记过程之后,CMS无法在当次收集中处理它们,只好留待下一次GC时再清理。(所以也需要预存空间存放这些浮动垃圾。)

    3. CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。不过CMS顶不住要FullGC时会开启内存碎片的合并整理过程。内存整理的过程是无法并发的,所以停顿的时间又会边长。



    G1收集器(垃圾区域Region优先)(整个堆)

    ——————– 可预测的停顿时间,整个堆划分成多个区域 ——————–

    G1(Garbage - First)是一款面向服务端应用的垃圾收集器。基于“标记 - 整理”算法。

    它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离了,它们都是一部分Region(不需要连续)的集合。

    G1(Garbage - First)名称的由来是G1跟踪各个Region里面的垃圾堆的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region

    这种使用Region划分内存空间以及有优先级的区域回收方式,保证G1收集器在有限的时间内可以获得尽可能高的收集效率。

    与其它GC收集器相比,G1具备如下4个特点 :

    • 并行与并发
      使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

    • 分代收集
      与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。新生代和老年代不再是物理隔离了,是多个大小相等的独立Region。

    • 空间整合
      与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

    • 可预测的停顿
      这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。(后台维护的优先列表,优先回收价值大的Region)

    并且在G1收集器中,每个Region都有一个与之对应Remember Set来避免全堆扫描。并且还可以根据这个Remember Set来检查Reference 引用的对象是否处于不同的Region之中。


    这里写图片描述

    G1收集器和CMS收集器运作过程有很多相似之处,整个过程也分为4个步骤 :

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

    • 并发标记
      并发标记阶段就是进行GC Roots Tracing的过程。(从GC Roots 开始对堆进行可达性分析,找出存活对象。)

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

    • 筛选回收
      首先对各个Region的回收价值和成本进行排行,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

    如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。



    垃圾收集器组合

    Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。


    这里写图片描述

    图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。

    虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

    转载:https://blog.csdn.net/qian520ao/article/details/79050982

    猜你喜欢

    转载自blog.csdn.net/qq_34417408/article/details/86616612