JVM GC

作者: Aiibai | 来源:发表于2019-01-08 19:51 被阅读0次

    GC需要完成三件事情

    • 那些内存需要回收
    • 什么时候回收
    • 怎么回收
    1. 如何判断一个对象可以回收了

    有两种方式:引用计数算法 根搜索算法

    • 引用计数法:
      对象需要添加一个引用计数器,有地方引用它时,引用计数加1,如果引用失效,引用减1,如果计数为0,代表可以清除了。
      存在的问题:循环引用

    • 根搜索算法
      这个算法的基本思路是通过一系列的名为 GC Roots 的对象作为起点,从这些对象开始往下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,证明此对象是不可用的。
      这时候有一个问题,那些对象可以作为 GC Roots

      • 虚拟机栈(栈帧中的本地变量表)中引用的对象
      • 方法区中的类静态属性引用的对象
      • 方法区中的常量引用的对象
      • 本地方法栈中JNI(即一般说的 Native 方法)中引用的对象

    既然两种判断对象是否可用都与引用有关,那么究竟都有那些引用呢?

    • 强引用(Strong Reference),只要强引用还存在,垃圾收集器永远都不会回收掉被引用的对象。
    • 软引用(Soft Reference), 在系统发生内存溢出之前,将会把这些引用列入到回收范围之中并进行第二次回收。
    • 弱引用(Weak Reference), 这些被引用的对象只能生存到下一次垃圾收集器发生之前。
    • 虚引用(Phantom Reference),一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取到一个对象实例。

    最后,如果对象不可达,一定会非死不可吗,不一定。
    要正真觉得一个对象死亡,还需要至少两次标记过程。
    如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize()方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机认为这两种情况没有必要调用。
    将有必要执行 finalize() 方法的对象放入名为 F-Queue 的队列中,稍后虚拟机调用这些对象的 finalize() 方法,这个时候可以拯救一些对象,将需要拯救的对象与引用链建立关系。稍后 GC 将对 F-Queue 中的对象进行第二次的标记,被拯救的对象会被移除集合中。

    2.垃圾收集算法
    • 标记清除(Mark-Sweep
      image.png

    优点:

    • 简单
    • 其他都是基于这个算法进行的改性。

    缺点:

    • 效率问题,标记的和清除的效率都不高

    • 空间问题,标记清除会产生大量的内存碎片,内存碎片可能存在的问题是:当有大对象需要分配时,找不到足够大的连续内存而不得不提前触发另一次垃圾收集动作。

    • 复制算法(Copying
      它将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的空间一次清理掉。

      image.png

    优点:

    • 简单高效

    缺点:

    • 可用内存缩小为原来的一半
    • 当对象存活率较高率,效率较低
    • 需要担保空间

    应用:
    IBM研究表明,新生代 98% 的对象都是朝生夕死的,所以并不需要 1:1 来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 EdenSurvivor 中还存活的对象一次性拷贝到另一块 Survivor 空间上,最后清理掉 Eden 和刚才使用过的 Survivor 空间。 HotSpot 虚拟机默认 EdenSurvivor 大小比例为 : 8:1。当 Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)。

    • 标记-整理(Mark-Compact
      标记阶段与 标记-清除算法 一样,但是整理阶段并不是对可回收对象进行清理,而是:让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。适用于对象存活率比较高的场景。

      image.png
    • 分代收集(Generational Collection
      针对对象的存活周期将内存划分为几块,一般为 新生代老年代,根据不同快对象的存活特点,选择合适的收集算法。新生代的算法存活率比较低,可以选择 Copying老年代 的对象存活率高,并且没有担保空间,可以选择 Mark-Compact算法。

    3. 垃圾收集器

    java 虚拟机规范没有对垃圾收集器应该如何实现做出规定,因此不同厂商、不同版本的虚拟机所提供的垃圾收集器可能会差别很大,下面的讨论是基于 Sun HotSpot 虚拟机。

    垃圾收集器

    图中如果两个垃圾收集器之间存在连线,表明它们可以搭配使用。

    Serial 收集器
    是(JDK 1.3.1 之前)是虚拟机新生代收集的唯一选择。是单线程收集,单线程不仅仅表示只会使用一个 CPU或一条收集线程完成垃圾收集工作,更重要的是它在收集时,其他所有工作线程都要停止(Stop The World)。

    Serial/Serial Old 收集器运行示意图

    缺点:

    • 多核时效率低
    • 用户体验不好

    优点:

    • 对于单核,省去了线程上下文切换的开销

    使用场景:
    对于 Client 模式下的虚拟机来说,由于分配的内存较小,停顿时间较短。

    ParNew 收集器
    ParNew 收集器Serial 收集器的多线程版本,其余行为完全一样(包括:控制参数、收集算法、Stop The World、对象分配规则、回收策略等)。

    ParNew/SerialOld 收集器运行示意图

    ParNew 收集器Server 模式下的虚拟机首选的新生代垃圾收集器,即使它相对 Serial 收集器 没有太多的改进,因为除了 Serial 收集器,只有它能与 CMS 收集器 搭配使用。
    ParNew 收集器 也是使用 -XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用 -XX+UseParNewGC 选项强制指定它。
    默认 ParNew 收集器 开启的收集线程与 CPU 的数量相同,也可以通过 -XX:ParallelGCThreads 指定。

    ParNew 收集器 是并行收集器, CMS 收集器 是并发收集器。
    并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    并发(Concurrent) :指用户线程和垃圾收集线程同时执行。

    Parallel Scavenge 收集器
    Parallel Scavenge 收集器 也是一个新生代收集器,使用复制算法,也是并行多线程收集。不过它的一个显著的特点是:其他垃圾收集器关注的是尽量缩短用户线程的停顿时间,而它关注的是吞吐量。所谓吞吐量就是CPU用于执行用户线程所用时间与CPU总消耗时间的比值(吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间))。

    停顿时间越短越适合于交互式程序,而高吞吐量可以高效的使用CPU,适合计算密集型程序。

    Parallel Scavenge 收集器 提供了两个参数用于精确控制吞吐量,分别是:-XX:MaxGCPauseMillis-XX:GCTimeRatio
    除了上面的两个参数外,有另外一个参数比较重要 :-XX"+UseAdptiveSizePolicy ,当这个参数打开以后,就不需要手动指定新生代的大小(-Xmn) 、EdenSurvivoer 的比例(-XX:SurvivorRatio)以及晋升老年代的对象年龄(-XX:PrertenureSizeThreshold)等细节参数。虚拟机将会根据设定的最大GC停顿时间或者吞吐量,自动调整。

    Serial Old 收集器
    Serial Old 收集器Serial 收集器的老年代版本,同样是单线程的,使用标记整理算法。主要适用于 Client 模式下的虚拟机,在 Server 模式下,主要有两个用途:1. 在 JDK 1.5 以及之前的版本中,与 Parallel Scavenge 收集器 搭配使用。2. 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 的时候使用。

    Serial/Serial Old 收集器运行示意图

    Paralledl Old 收集器
    Parallel Scavenge 收集器 的老年代版本。多线程标记整理吞吐量优先收集器。

    image.png

    CMS 收集器
    CMS 收集器 是一种以获取最大回收停顿时间为目标的收集器。是基于 标记-清除算法实现的,收集过程分为四个阶段:

    • 初始标记(CMS initial mark
    • 并发标记(CMS concurrent mark
    • 重新标记(CMS remark
    • 并发清除(CMS concurrent sweep
      其中初始标记和重新标记阶段仍然需要 Stop The World
      初始阶段:只是记录一下 GC Roots 直接关联的对象,速度很快。
      并发标记:进行 GC Tracing 过程。
      重新标记:为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿时间一般会比初始标记阶段稍微长一点,但远比并发标记时间段。
      Concurrent Mark Sweep

    缺点:

    • 对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停止,但是会因为占用了一部分CPU资源,导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程最多占用不超过 20%的资源,但是当CPU不足4个时,对应用程序的执行速度影响就比较大了。
    • 无法处理浮动的垃圾(Floating Garbage),可能出现 Concurrent Mode Failure 失败而导致另一次Full GC的产生。由于 CMS 并发清理阶段用户线程还在运行着,伴随着程序的运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法再本地收集中处理它们,只好留在下一次GC时清理掉。这一部分垃圾称为 “浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即还需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全填满时再进行收集,需要预留一部分空间提供给在并发运行时程序运行使用。在默认设置下,CMS收集器的老年代使用68%的空间后会触发GC,可以调高-XX:CMSInitiatingOccupancyFraction 参数,降低GC次数。要是GMS运行期间预留的内存无法满足程序需要,就会出现一次 “Concurrent Mode Failure” 失败,这时候会启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿的时间就很长了。
    • 产生大量的空间碎片,这是使用 标记-清理算法固有的缺点。为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数,在每次Full GC以后进行一次碎片整理。这样停顿时间不得不变长了。接着又出现另外一个参数:-XX:GMSFullGCsBeforeCompaction ,表示执行多少次 Full GC以后执行一次碎片整理。

    G1 收集器
    https://www.jianshu.com/p/e99000058840
    http://www.importnew.com/27793.html

    image.png

    相关文章

      网友评论

          本文标题:JVM GC

          本文链接:https://www.haomeiwen.com/subject/drwbrqtx.html