1.回收算法

1.1 标记清除(Mark-Sweep)

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段
在标记阶段首先通过根集合节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
image.png
适用场合

  • 存活对象较多的情况下比较高效
  • 适用于老年代(即旧生代)

缺点

  • 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
  • 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)

1.2 复制算法

“半区复制”(Semispace Copying)
从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉。
image.png
现在的商业虚拟机都采用这种收集算法来回收新生代。
适用场合:

  • 存活对象较少的情况下比较高效
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 用于新生代(即新生代):基本上 98% 的对象是“朝生夕死”的,存活下来的会很少

缺点:

  • 需要一块儿空的内存空间
  • 需要复制移动对象

    1.3 标记整理(Mark-Compact)

    复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。
    这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
    image.png
    标记-压缩(Compact)算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。
    首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,其性价比比较高。

分析:

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

吞吐量的实质是赋值器(Mutator,可以理解为“用户程序”或“用户线程”)与收集器的效率总和。

  • 即使不移动对象会使得收集器的效率提升一些,但因内存分配和访问相比垃圾收集频率要高得多,这部分的耗时增加,总吞吐量仍然是下降的。
  • HotSpot 虚拟机里面关注吞吐量的 ParallelScavenge 收集器是基于标记-整理算法的,而关注延迟的 CMS 收集器则是基于标记-清除算法的,这也从侧面印证这点。

复杂的内存分配器和内存访问器: 譬如通过“分区空闲分配链表”来解决内存分配问题(计算机硬盘存储大文件就不要求物理连续的磁盘空间,能够在碎片化的硬盘上存储和访问就是通过硬盘分区表实现的)。

2.回收机制

2.1 分代收集理论

分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储,每次只回收其中某一个或者某些部分的区域:

  • 因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;
  • 也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。

分代理论第三假设:

  • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

因此,存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。不应再为了少量的跨代引用去扫描整个老年代,在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。Minor GC 时,只有包含了跨代引用的小块内存里的对象才会被加入到 GCRoots 进行扫描。

2.2 年代划分

HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden 的 80% 加上一个 Survivor 的 10%),只有一个 Survivor 空间,即 10% 的新生代会被“浪费”。

当然,98% 的对象可被回收仅仅是“普通场景”下测得的数据,Appel 式回收有一个“逃生门”的安全设计,当 Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。

新生代(Young)、老年代(Old)是HotSpot虚拟机,也是现在业界主流的命名方式。
在IBM J9虚拟机中对应称为婴儿区(Nursery)和长存区(Tenured),名字不同但其含义是一样的。

新生代分为 Eden 区和 survivor 区(两块儿:from 和 to),且 Eden:from:to = 8:1:1。

GC 算法和回收机制 - 图4

新建对象内存分配过程:

  • 新产生的对象优先分配在 Eden 区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入老年代);
  • 当 Eden 区满了或放不下了,这时候其中存活的对象会复制到from区。
    这里,需要注意的是,如果存活下来的对象from区都放不下,则这些存活下来的对象全部进入老年代。之后 Eden 区的内存全部回收掉。
  • 之后产生的对象继续分配在 Eden 区,当 Eden 区又满了或放不下了,这时候将会把 Eden 区和 from 区存活下来的对象复制到 to 区(同理,如果存活下来的对象 to 区都放不下,则这些存活下来的对象全部进入老年代),之后回收掉 Eden 区和 from 区的所有内存。
  • 如上这样,会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了 15 次(这个次数可以通过:-XX:MaxTenuringThreshold 来配置),就会进入老年代了。
  • 当老年代满了或者存放不下将要进入老年代的存活对象的时候,就会发生一次 Full GC(这个是我们最需要减少的,因为耗时很严重)。

    3.垃圾回收类型

    《深入理解 Java 虚拟机》统一概念:
    1.部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。

  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。

2.整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

3.1 Minor GC

对新生代进行回收,不会影响到老年代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。

3.2 Full GC

也叫 Major GC,对整个堆进行回收,包括新生代和老年代。
由于 Full GC 需要对整个堆进行回收,所以比 Minor GC 要慢,因此应该尽可能减少 Full GC 的次数。
导致 Full GC 的原因包括:

  • 老年代被写满
  • 永久代(Permanent)被写满
  • System.gc() 被显式调用等

    4.垃圾回收算法总结

    4.1 新生代:复制算法

  1. 所有新生成的对象首先放在新生代, 其目标就是尽可能快地收集生命周期短的对象;
  2. 新生代内存按照 8:1:1 分为一个eden区和两个survivor区。大部分对象在Eden区中生成,回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空,如此往复。
  3. 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
  4. 新生代发生的 GC 也叫做 Minor GC,MinorGC 发生频率比较高(不一定等Eden区满了才触发)。

    4.2 老年代:标记-清除 / 标记-整理

    1) 在新生代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
    2) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发 Major GC 即 Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
    以上这种新生代与老年代分别采用不同回收算法的方式称为“分代收集算法”,这也是当下企业使用的一种方式。

每一种算法都会有很多不同的垃圾回收器去实现,在实际使用中,根据自己的业务特点做出选择就好。