JVM 有哪些垃圾回收算法?

116 阅读7分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

JVM 一次完整的GC

1.新生代开始执行垃圾回收,堆中的Eden和其中一个Survivor区的存货对象放置到另一个Survivor区中,将这两个区清空。

2.如果Survivor区中放不下这么多对象,就会将多余的对象放置到老年代中(如果是大对象,就直接存放到老年代中)

3.Survivor中的对象每经过一次收集,幸存下来的对象年龄+1,当到达一定的年龄,就会晋升到老年代。(CMS 年龄6, Parallel年龄15)

什么是垃圾

  • 什么是垃圾?

没有任何引用指向的一个对象或者多个对象(循环引用);

如何定位垃圾

引用计数法

在对象添加一个引用计数器。当有一个地方引用,就+1,引用失效就-1,。当计数器为0时,代表不可能被使用。

缺点:有很多额外情况需要考虑,需要配合大量的额外处理才能使用。比如,两个对象循环依赖,就不可能回收

可达性分析

从“GC Roots”根对象作为起始节点集,从这些节点触发根据引用关系向下搜索,走过的路径为“引用链”。如果某个对象到起始节点没有任何引用链,说明这个对象不可能再被使用

  • 哪些对象可以作为GC Root根节点?
  • 虚拟机栈中引用的对象,比如当前正在运行的方法使用到的参数,局部变量,临时变量等。
  • 方法区中类静态属性引用的对象。 static String name = new String();
  • 方法区中常量引用的对象 final Integer i = new Integer();
  • 本地方法栈中JNI(native方法)引用的对象
  • 虚拟机内部的引用,如基本数据类型对应的Class对象,常驻的异常对象,NullPointException,系统类加载类。
  • 所有被同步锁持有的对象
  • 反应Java虚拟机内部情况的JMXBean。JVMTI中注册的回调,本地代码缓存等。

这些都可以放进GC Root根节点集合中,不仅这些,将一些关联区域的对象也一并加入。

局部回收,避免GC Roots包含过多的对象而多度膨胀。

垃圾算法

这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。

Mark-Sweep (标记清除)

标记出垃圾之后清除

缺点:位置不连续,产生碎片

Copying(拷贝)

应用场景

用于新生代高效性是建立在存活对象少、垃圾对象多的前提下的

过程

将活着的内存空间分为两块,每次只使用其中一块,将随机分布的正在引用的内存地址拷贝到连续的内存空间中,然后将之前的随机内存地址全部清空

所以采用的是指针碰撞

优点:没有碎片,速度快

缺点:浪费空间,内存是能使用一半

Mark-Comapct(标记压缩)

应用场景

用于老年代

过程

1.标记出所有垃圾

2.将其他的正在引用的内存地址拷贝到这个垃圾分类地址上,形成连续的正在引用内存地址。

优点: 集合了标记清楚和拷贝的优点

缺点:效率比较拷贝算法低

Mark-SweepMark-CompactCopying
速度快慢中等最慢最快
空间开销少(会堆积碎片)少(不堆积碎片)通常需要活对象的2倍大小(不堆积碎片)
移动对象

Generational Collection(分代收集)算法

是目前大部分JVM的垃圾收集器采用的算法。将垃圾区域分成老年代和新生代。

  • 特点

老年代的特点是回收时只有少量垃圾需要回收,新生代时有大量垃圾需要回收

  • 区域使用的算法

老年代使用标记压缩算法

新生代使用复制算法

垃圾收集器

image.png

JDK1.8 默认的垃圾回收器 :Parallel Scavenge + Parallel Old ,所以调优主要针对的就是它。

useParallelGc

image.png

新生代垃圾收集器

新生代的垃圾回收器都是基于复制算法

  • 1.Serial:单线程的垃圾回收器,STW (stop-the-world)停止所有线程

    • 优点

    客户端模式下默认的新生代收集器,简单高效(相比于其他收集器的单线程)

    内存占用最小

    最高的单线程收集效率:没有线程交互的开销

  • 2.ParNew

    • Serial 的 多线程并行版本,在单线程情况下效率比不上Serial。

      处理器核心越多,它的效率越高,因为是并行。默认开启收集线程数 = 处理器核心数。

    • 和CMS 配合使用。

  • Parallel Scavenge

    • 和ParNew非常相似,但是关注点不同:达到一个可控制的吞吐量:运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)这个值默认99,越高越好。
    • 和Parallel Old搭配使用

老年代垃圾收集器

老年代的垃圾收集器都是基于 标记压缩算法

  • 1.Serial Old

    Serial的老年代版本。

  • 2.Parallel Old

    Parallel Secavenge 的老年代版本,与Parallel Secavenge 搭配使用,组成吞吐量优先的搭配组合

  • 3.CMS

CMS

ConcurrentMarkSweep,以获取最短回收停顿时间为目标的收集器。基于标记清理算法(不是标记压缩)

用于服务端,关注服务器响应时间,希望系统停顿时间尽量少垃圾回收运行期间用户线程也能运行

四个步骤
  • 初始标记

    • 找到GC Roots能关联的类,这个阶段也会STW ,但是时间很短。
  • 并发标记

    • 并发的可达性分析,基于三色标记算法,这个最耗时

    • 垃圾回收线程和业务线程同时执行

    • 产生的并发问题

    1.当业务线程使用对象的时候垃圾线程也标记了这个对象,那么垃圾线程会认为这个线程存活对象,但是业务线程用完后就不再使用了,这个垃圾还是标记为存活对象

    2.垃圾线程标记对象为垃圾,但是这时业务线程又使用了这个对象。那么这个在使用的对象就会标记称垃圾。

  • 重新标记

    • 修正漏标和错标,这个阶段STW,但是时间也不长
  • 并发清除

    • 使用标记清除算法,将标记的对象直接删除,和用户线程并发执行。由于使用清除算法,会产生碎片
    • 为何不适用压缩算法? 如果使用压缩算法,会涉及到用户线程正在使用,情况更加复杂,用户线程不停顿很难处理
    • 可能会出现浮动垃圾:在清除阶段用户产生的新垃圾,但是CMS在本次垃圾回收中无法处理,只能等到下次CMS回收。
并发模式失败

CMS 无法处理浮动垃圾可能会"Concurrent Mode Failure”失败,导致Full GC

失败原因:

1.对象提升到老年代的速度很快,使得CMS 不能保持老年代足够的空间

2.大量碎片化导致没有足够的空间容纳提升上来的新对象。

年老代将进行垃圾收集以释放可用空间,同时也会整理压缩以消除碎片,这个操作需要停止所有的java应用线程,并且需要执行相当长时间。

  • 三色标记
  • 黑色:对象已经被收集器访问过,并且它的所有引用也已经访问过,代表它是线程安全的。
  • 灰色:表示对象已经被垃圾收集器访问过,但是至少存在一个引用还没有被扫描过
  • 白色:这个对象还没有被扫瞄过。

开始的时候,会任务所有的对象都是白色的; 利用根可达算法将根下的引用对象都标记为灰色 移动到 灰色对象中,将本对象 标记为黑色; 将灰色对象中所有引用对象标记为灰色; 重复第3第4步,直至扫完所有灰色对象; 完成后没有被标记为黑色的,或者全部是白色的,就为mark sweep出来的垃圾对象可以进行回收。