了解垃圾回收算法前,需要先了解评价垃圾回收算法的评价标准和指标。有以下几点:

  • 吞吐量
  • 最大暂停时间
  • 内存使用效率(堆使用效率)
    在这里不做过多介绍。

1. 总览

垃圾回收算法的整体迭代历史如下:

  1. 标记-清除算法(Mark Sweep GC):于1960年发布;
  2. 复制算法(Copying GC):于1963年发布;
  3. 标记-整理算法(Mark Compact GC)
  4. 分代GC(Generational GC)

2. 标记-清除算法(Mark Sweep GC)

标记-清除算法核心分为两个阶段:

  1. 标记阶段:将所有存活的对象进行标记;
  2. 清除阶段:从内存中删除没有标记的对象,即非存活对象。

优点

很明显,实现简单。整体逻辑就是标记存活对象,然后删除非存活对象即可。

缺点

  1. 碎片化问题严重:由于内存是连续的,在对象被删除后,内存中会出现很多内存碎片,如果需要分配一个较大的对象的话,可能会出现,剩余总量够,但依然无内存可分配的情况;
  2. 分配速度太慢:其实也是碎片化的问题造成的。由于内存碎片的存在,要想分配空间,就必须维护一个表用于记录空闲内存。有一定可能需要遍历整个表才能获得合适的内存空间。

3. 复制算法(Copying GC)

为了解决标记-清除算法的问题,又发布了一款复制算法。该算法的核心思想是:

  1. 准备两块空间作为From空间To空间
  2. 在垃圾回收阶段,将From中存活的对象复制到To空间中;
  3. 然后将两块空间的名字互换。
    如此往复,就能实现每次回收后出现大量内存碎片的问题。

优点

  1. 不会发生碎片化:如上所说,复制算法在复制之后会按照顺序存到To空间中,因此不会出现碎片空间;
  2. 吞吐量高: 复制算法只需要遍历一次存活对象复制到To空间即可,无需重新整理,因此性能较好。

缺点

内存使用效率低: 不难发现,使用复制算法,每次只能真正使用一半的内存来存储对象。

4. 标记-整理算法(Mark Compact GC)

为了解决复制算法的内存使用效率较低的问题,又发布了 标记-整理算法。它的核心思想分为两阶段,如下:

  1. 标记阶段:将所有存活的对象进行标记。
  2. 整理阶段: 将存活对象移动到堆的一端,然后清理掉剩下的内存空间。

优点

  1. 内存使用效率高: 整个内存都可以使用,不会像复制算法那样只能使用半个内存;
  2. 不会发生碎片化: 整理阶段可以将内存碎片去除。

缺点

整个阶段的效率不高,整理阶段的整理算法性能整体不高,可能会耗费较多时间。

5. 分代GC(Generational GC)

通过以上几种方案,不难发现,在实际使用场景下很难说某一种算法好或者坏。根据不同场景不同特性,各有优势。因此,分代GC被提出。该算法思想如下:

  1. 将内存分为年轻代老年代两个部分,其中年轻代再分为Eden区幸存区(幸存区采用复制算法);
  2. 对象创建出来后,首先会被放入Eden区;
  3. 随着Eden区的对象逐渐增多,如果Eden区满了,新创建的对象已经无法放入,就会出发年轻代的GC,即Young GC或Minor GC;
  4. Young GC会把Eden区和幸存区的From区中需要回收的对象回收,剩下的放入幸存区的To中。(并且会把对象年龄超过15的放入老年代)。

优点

此算法融合了几种算法的思想,根据不同对象的特点进行选择。优点如下:

  1. 可以通过调整年轻代和老年代的大小比例来适应不同类型的应用程序,提高内存的利用率和性能;
  2. 可以针对年轻代和老年代对象的不同特点,选择不同的回收算法。年轻代一般选择效率高、不会产生内存碎片的复制算法;老年代可以选择标记-清除和标记-整理算法。提高了开发人员的选择灵活度。
  3. 分代的设计中,允许只对年轻代进行回收,如果能满足对象分配的要求,就没有必要对整个堆进行回收。回收的范围小,那么STW(Stop The World)引起的程序停顿时间也就小了。

这三点可以简单理解为:在分代内存区域的大小上更加灵活;在回收算法的选择上更加灵活;在回收范围上更加灵活。

6. 如何判断对象是否存活?

^6c2b0b

在以上的算法介绍中,不难发现,要想回收非存活对象,就必须得确定对象是否存活。那么是如何判断对象是否存活的呢?有以下两种方案:

  1. 引用计数法;
  2. 可达性分析法。

目前Java中的方案是可达性分析法

6.1 引用计数法

引用计数法就是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

如上图中,water对象的引用计数器就是2。当它的引用计数器变成0的时候,它便可以被回收。但是此方案可能会有一个问题,如下图所示:

一旦两个对象相互引用,即循环引用。那么二者的引用计数器则永远不可能为0,也永远无法被回收,即使没有人使用了,也无法回收。

6.2 可达性分析法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图所示:

那么GC Roots是哪些对象呢?

作为GC Roots的对象主要包括下面几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等;
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量;
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用;
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。