JVM之垃圾收集算法与垃圾收集器(四)
  • 1.垃圾收集算法

    从如何判定对象消亡的角度出发,垃圾收集算法可以划分为引用计数式垃圾收集追踪式垃圾收集,由于引用计数式垃圾收集在主流JVM并未涉及,所以追踪式垃圾收集为主。

    当前的JVM大多数遵循了分代收集理论进行设计,它主要建立在两个分代假说上面:弱分代假说强分代假说。分代假说奠定了收集器的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储。

1.1 标记-清除算法

标记过程:遍历所有的GC Roots,然后将GC Roots可达对象标记为存活的对象。

清除过程:将没有标记的对象全部清除。

主要缺点:

  • 执行效率不稳定
  • 内存空间碎片化问题

1.2 标记-复制算法

也被简称为复制算法,它将可用内存划分成大小相等的两块,每次只使用其中的一块,当这一块的内存用完,就把还存活的对象复制到另外一块上面,然后再把这一块内存全部清除。这种算法有优有劣

  • 优点:不会出现内存碎片的问题
  • 缺点:可用内存缩为原来的一半,浪费空间

    为了提高空间利用率的问题,可以将新生代分为一块较大的Eden区,和两块较小的Survivor区,比例为8:1:1,每次 分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor区,然后清理掉Eden和已用过的Survivor区,这样只有10%的内存被浪费掉。
    但是不能保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时,需要依赖其他内存区域(老年代)进行分配担保。

    1.3 标记-整理算法

    标记过程:与标记-清除算法一样,将存活的对象标记。
    整理过程:让所有存活的对象都向内存空间一端移动,然后直接清理掉边界之外的内存。

    这是一种老年代的垃圾收集算法,老年代对象的生命周期较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次效率很低。

    2. 经典垃圾收集器

    2.1 新生代垃圾收集器

    2.1.1 Serial收集器(单线程)

    Serial收集器是一个新生代垃圾收集器,它在垃圾收集时,必须暂停其它所有工作线程,知道它收集结束(Stop the World)。
    对于内存资源受限的环境,它时所有收集器里面额外内存消耗最小的,对于单核处理器或者处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,壮美做垃圾收集自然可以获得最高的单线程手机效率。

    2.1.2 ParNew收集器(多线程)

    ParNew收集器实质上时Serial收集器的多线程并行版本,由多条GC线程并行的进行垃圾清理,清理过程仍需Stop The World。在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。

    2.1.3 Parallel Scavenge收集器(多线程)

    Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。但是两者有巨大的不同点:

  • Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
  • ParNew:追求降低用户停顿时间,适合交互式应用。

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

2.2 老年代垃圾收集器

2.2.1 Serial Old收集器(单线程)

Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程,都适合客户端应用。它们唯一的区别就是:Serial Old 工作在老年代,使用标记-整理算法;Serial 工作在新生代,使用标记-复制算法。

2.2.2 Parallel Old收集器(多线程)

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。

2.2.3 CMS收集器

CMS(Concurrent Mark Sweep,并发标记清除)收集器是一种以获取最短回收停顿时间为目标的收集器,它在垃圾收集时用户线程和GC线程并发执行,因此在手机过程不会有明显的卡顿。
CMS收集器是基于标记-清除算法实现的。它的运作过程分为四个步骤:

  • 初始标记:Stop The World,仅使用一条初始标记线程对所有与GC Roots直接关联的对象进行标记
  • 并发标记:使用多条标记线程,与用户线程并发执行,标记出所有废弃的对象。
  • 重新标记:Stop The World,为了修正并发标记期间,由于用户进程继续运作而导致标记产生变动的那一部分对象的标记记录。
  • 并发清除:清理删除掉标记阶段判断已经死亡的对象,这个阶段也是可以与用户线程同时并发的。

    并发标记和并发清除阶段耗时最长,且可以与用户线程一起工作,总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

    缺点:

  • 吞吐量低
  • 无法处理浮动垃圾,导致频繁Full GC
  • 使用标记-清除算法,会有大量的空间碎片产生

    对于空间碎片的问题,可以通过开启-XX:+UseCMSCompactAtFullCollection,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的。这样空间碎片问题解决,但是停顿时间又会变长,因此还有一个参数-XX:CMSFullGCsBefore-Compaction,作用是要求CMS收集器在执行过若干次不整理空间的Full GC之后,下一次进入Full GC之前进行碎片整理。

    2.2.4 Garbage First收集器

    G1是一款主要面向服务端应用的垃圾收集器,他没有新生代和老年代的概念,它可以面向对内存任何部分来组成回收集(CSet)进行回收,它把连续的Java堆划分为多个大小相等的独立区域(Region),Region中还有一类特殊的Humongous区域,专门用来存储大对象,G1认为只要大小超过一个Region容量一半的对象即可判定为大对象。

    G1收集器还有细节问题需要妥善解决:

  • 将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?

    使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用要复杂很多,每一个Region都有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别再哪些卡页的范围之内记忆集再存储结构的本质上是一种哈希表,key是Region的起始地址,value是一个集合,存储的是卡表的索引号。

  • 在并发阶段如何保证收集线程与用户线程互不干扰地运行?

    G1用过原始快照(SATB)算法来实现的。

    G1收集器的运作过程分为以下几个步骤:

  • 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。并且修改TAMS指针的值。让下一个阶段用户线程i把那个发运行时,能正确地在可用地Region中分配新对象。

  • 并发标记:使用一条标记线程与用户线程并发执行,此过程进行可达性分析,递归扫描整个堆中地对象图,找出要回收的对象,耗时较长,能与用户线程并发执行,扫描完成之后,需要重新处理SATB记录下的并发时有引用变动地对象。
  • 最终标记:Stop The World,对用户线程做出另一个短暂的暂停,用户处理并发阶段结束后仍遗留下来的SATB记录。
  • 筛选回收:负责更新Region的统计数据,Region的回收价值和成本进行排序,把回收的那一部分Region的存活对象复制到空的Region中,在清除掉旧Region的全部空间,整个过程Stop The World。

    3. 低延迟垃圾收集器

    衡量垃圾收集器的三项最重要指标是:内存占用,吞吐量和延迟。三者构成了一个不可能三角,优秀的垃圾收集器通常最多可以同时达成其中的两项。延迟是垃圾收集器最被重视的性能指标

3.1 Shenandoah收集器

非官方的收集器,Oracle拒绝在OracleJDK 12中支持Shenandoah收集器,只有在OpenJDK才会包含。
它和G1有着相似的堆内存布局,在初始标记,并发标记等许多阶段的处理思路都高度一致,但是有三个明显的不同之处:

  • 支持并发的整理算法,G1的回收阶段是可以多线程并行的,但是不能与用户线程并发。
  • 默认不使用分代收集,不会有新生代Region和老年代Region的存在
  • 摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为”连接矩阵”的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针的记忆集维护,也降低了伪共享问题的发生概率。

3.2 ZGC收集器

它和Shenandoah收集器的目标是高度相似的,都希望在尽可能对吞吐量影响不大的前提下,尽量减少延迟。ZGC也采用基于Region的堆内存布局,但是ZGC的Region具有动态性(动态创建和销毁),以及动态的区域容量大小。ZGC的并发整理算法通过染色指针计数实现。ZGC地运作过程可以分为四个阶段,四个阶段都可以并发执行。两个阶段之间会有短暂的停顿。

  • 并发标记:与G1一样,遍历对象图做可达性分析,ZGC的标记是在指针上进行的,而不是在对象上进行
  • 并发预备重分配:根据特定的查询条件统计出本次收集要清理哪些Region,将这些Region组成重分配集
  • 并发重分配:重分配阶段把重分配集中存活对象复制到新的Region上,并为重分配集中的每一个Region维护一个转发表,记录从旧对象到新对象的转向关系。
  • 并发重映射:修正整个堆中指向重分配集的就对象的所有引用。
文章作者: L1nker4
文章链接: https://l1n.wang/jvm04-gc-algorithrms/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 L1nker4