JVM 学习总结(五)——垃圾收集算法

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为 “引用计数式垃圾收集”(Reference Counting GC)“追踪式垃圾收集”(Tracing GC) 两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。
注: 以下介绍的所有算法均属于追踪式垃圾收集的范畴。

一、垃圾收集算法

1、分代收集理论

分代收集(Generationl Collection)建立在三个分代假说之上:

  1. 弱分代假说(Weak Generationl Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generationl Htpothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

Java堆划分出不同区域之后的回收类型:

  1. 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
    - 新生代收集(MinorGC/Young GC):指目标只是新生代的垃圾收集。
    - 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。“Major GC”需按上下文区分到底是指老年代的收集还是整堆收集。
    - 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  2. 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

注:通常能单独发生垃圾收集行为的只是新生代,除了CMS收集器,其他都不存在只针对老年代的收集。

2、标记-清除算法

标记-清除(Mark-Sweep)算法: 标记-清除算法是最早出现也是最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:

  1. 首先标记出所有需要回收的对象。
  2. 在标记完成后,统一回收掉所有被标记的对象。
    也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。“标记-复制算法”和“标记-整理算法”都是以标记-清除算法为基础,对其缺点进行改进而得到的。

缺点:

  1. 执行效率不稳定。如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随着对象数量增长而降低。
  2. 内存空间的碎片化问题。标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
    标记-清除算法的执行过程如下图:

”标记-清除“算法示意图

3、标记-复制算法

标记-复制算法常被简称为复制算法,现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。

  1. 半区复制(Semispace Copying)算法: 半区复制的垃圾收集算法是为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

    优点: 实现简单,运行高效。
    缺点: 这种复制回收算法的代价是将可用内存缩小为了原来的一半,造成太多的空间浪费。

    标记-复制算法的执行过程如下图:
    标记-复制算法示意图

新生代“朝生夕灭”的特点更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1:1的比例来划分新生代的内存空间

  1. Apple式回收: 针对具备“朝生夕灭”特点的对象而提出的一种更优化的半区复制分代策略。Apple式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。98%的对象可被回收仅仅是“普通场景”下测得的数据,无法百分百保证每次回收都只有不多于10%的对象存活,因此Apple式回收还有一个充当罕见情况的“逃生门”的安全设计——当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的。
    HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代内存布局。

4、标记-整理算法

老年代一般不能直接选用标记-复制算法,因为老年代中的对象存活率较高,标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更为关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况。

标记-整理(Mark-Compact)算法: 该算法针对老年代对象的存亡特征而产生,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。“标记-整理”算法的执行过程如下图。
“标记-整理”算法示意图
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

是否移动回收后的存活对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂:

  • 若移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。
  • 若不移动和整理存活对象,弥散于堆中的存活对象导致的空间碎片化问题只能依赖更为复杂的内存分配器和内存访问器来解决。内存的访问时用户程序最频繁的操作,假如在这个环节上增加了额外的负担,就会直接影响应用 程序的吞吐量。

如何选择是否移动存活对象?

从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量【实质是赋值器(Mutator,可以理解为使用垃圾收集的用户程序)与收集器的效率总和】来看,移动对象会更划算。即使不移动对象会使得收集器的效率提升一些,但因内存分配和访问相比垃圾收集频率要高得多,这部分的耗时增加,总吞吐量仍然是下降的。

若关注吞吐量,则采用标记-整理算法,如Parallel Scavenge收集器;
若关注延迟时间,则采用标记-清除算法,如CMS收集器。

另外一种可以不在内存分配和访问上增加太大额外负担的解决方案是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经达到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。

注:
通常标记-清除算法也是需要停顿用户线程来标记、清理可回收对象的,只是停顿时间相对而言要来的短而已。

参考:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 周志明

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

luffylv

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值