程序计数器、虚拟机栈和本地方法栈的生命周期都是随着线程开始而开始,随着线程的终止而终止,因此这些区域无需考虑过多的回收问题,所以垃圾收集主要关注点是堆和方法区这部分内存。

1. 判断对象是否需要回收

垃圾收集器在进行垃圾回收前要先确定哪些对象是需要被回收的,常见的两种算法有引用计数算法和可达性分析算法,其中主流的算法是可达性分析算法,因为引用计数算法无法解决循环引用的问题。

1.1 引用计数算法

引用计数算法就是给对象添加一个引用计数器,当对象被引用时计数器会加1,当不再被引用时计数器则会减1,当计数器为0时就说明该对象需要被回收了。虽然该算法实现起来很简单并且效率也不低,但是由于该算法难以解决循环引用的问题(即A.prop = B;B.prop = A),所以目前主流的语言使用较多的算法还是可达性分析算法。

1.2 可达性分析算法

可达性分析算法规定了一系列“GC Roots”对象当作起始点,从这些对象开始向下搜索的路径称为引用链,当“GC Roots”到某个对象中间没有任何引用链就说明“GC Roots”到这个对象是不可达的,说明该对象需要被回收。Java语言中可以作为“GC Roots”对象的有以下几种:

  • 虚拟机栈的栈帧中本地变量表中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象

    1.3 引用详解

    引用按照引用强度由强到弱依次分为:强引用、软引用、弱引用和虚引用四种。

    1.3.1 强引用

    强引用是最常见的一种引用,用来描述必需对象,类似于A a = new A();这一类型的引用就是强引用。垃圾回收不会回收强引用对象。

    1.3.2 软引用

    软引用是用来描述有用但非必需对象,发生内存溢出之前会将软引用对象进行二次回收,如果经过二次回收后内存依然不够才会发生内存溢出。通过SoftReference类来实现软引用。

    1.3.3 弱引用

    弱引用的强度比软引用更弱,当垃圾收集器工作时无论当前内存是否够用都会回收弱引用对象。通过WeakReference类来实现弱引用。

    1.3.4 虚引用

    虚引用是强度最弱的引用,也被称为幽灵引用或幻影引用,该引用完全不会影响对象的生存周期,也无法通过虚引用来获取对象实例,设置虚引用的目的仅仅是为了在对象被回收时可以收到通知。通过PhantomReference类来实现虚引用。

    1.4 finalize()

    经过可达性分析算法分析后不可达的对象也不会直接被回收,而是要经过两次标记才会真正说明该对象需要被回收。
    当对象到GC Roots没有引用链时就会被第一次标记然后进行筛选,筛选依据为对象是否有必要执行finalize()方法(如果对象没有重写finalize()方法或者对象的finalize()方法被虚拟机调用过则说明该对象没有必要执行finalize()方法,此时该对象就被判定为需要被回收)。

如果对象经过筛选发现有必要执行finalize()方法,该对象就会被放入F-Queue队列中,并稍后会由一个虚拟机自动创建的、低优先级的Finalizer线程中执行这个对象的finalize()方法。但是线程并不会等待对象的finalize()方法执行完毕,这样是为了防止finalize()方法执行速度慢或发生死循环而导致F-Queue队列中其他对象一直等待,最终导致内存回收系统崩溃。稍后GC会对F-Queue队列进行第二次标记,如果对象在finalize()方法中与引用链中的任何对象建立了引用关系就会在第二次标记时被移出需要被回收的集合,如果没有建立则会被回收。

1.5 方法区回收

方法区(永久代)的垃圾收集效率极低,主要回收废弃常量和无用的类。

1.5.1 废弃常量

以字面量为例,假设常量池中有一个字符串”temp”,但是系统中没有任何一个String变量引用该字符串并且没有其他任何地方引用该字符串,那么这个字符串就时废弃常量,此时如果发生回收,该字符串就会从常量池中移出。

1.5.2 无用的类

一个类必须满足以下三个条件才能被称为无用的类:

  • 堆中不存在任何该类的实例
  • 加载该类的类加载器已经被回收
  • 该类对应的java.lang.Class对象在其他任何地方都没有被引用,在其他任何地方都无法通过反射访问该类

无用的类并不一定会被回收,可以通过-Xnoclassgc来进行控制。

2. 垃圾收集算法

2.1 标记-清除算法

标记-清除算法是最基础的垃圾收集算法(后续算法都是在该算法的基础上改进其缺点得来的),它分为标记和清除两个阶段:先标记出需要被回收的对象,标记完成后统一回收所有被标记的对象。该算法的缺点有以下两点:

  • 效率问题

标记和清除两个阶段的执行效率都不高

  • 空间问题

经过清除后会出现大量不连续的空间碎片,如果后续有大对象需要分配空间时会额外触发一次垃圾收集。

2.2 复制算法

复制算法是为了解决标记-清除算法的效率问题而出现的。该算法将可用内存空间分为大小相同的两块。每次只使用其中一块,当使用的那块空间满了后会将上面还没有被回收的对象复制到另一块空白的空间,然后对当前这整块空间
进行回收。该算法无需考虑内存碎片问题并且简单高效,但是缺点也很明显就是将内存大小缩小为了一半。新生代基本都采用复制算法进行回收,后面会详细解析。

2.3 标记-整理算法

当对象存活率较高的时候,复制算法每次复制都要复制较多的对象,效率会变低而且复制算法会浪费50%的空间。因此出现了标记-整理算法,该算法和标记-清除算法的标记阶段是一样的,不同的是标记完毕后该算法会进行整理而不是清除。具体来说就是标记完毕后存活的对象会全部移动到一端然后将边界以外的内存全部清空。

2.4 分代收集算法

该算法只是根据对象的不同生命周期将内存划分开,然后根据每个部分的特点分别选用合适的收集算法。一般是将堆内存划分为新生代和老年代。新生代中的对象每次回收都会回收大量对象,对象存活率不高因此可以选择复制算法;而老年代中对象存活率高则使用标记-清除或标记-整理算法。

3. HotSpot的具体实现

3.1 OopMap

可达性分析整个分析过程整个系统中对象的引用关系必须保持一致,因此GC进行过程中所有Java线程都必须停顿(这种情况称为”STW—Stop The World”)。停顿后由于主流虚拟机都是准确式GC,虚拟机并不需要检查完所有的GC Roots,在HotSpot中是通过名为OopMap的数据结构来使虚拟机直接知道哪里存放着对象引用,使其可以快速准确的完成GC Roots枚举。

3.2 安全点

有很多指令都会使对象的引用关系发生改变(即OopMap中的数据发生改变),但是如果为每一条指令都生成一个OopMap又会极大的增加空间压力。因此HotSpot只会在特定的位置来生成OopMap,这些位置被称为安全点。程序并不会随时随地的停下来进行GC,而是只有达到了安全点才进行停顿。主要的安全点有:方法返回前、调用某个方法后、抛出异常的位置、循环的末尾等。GC发生时有两种方案让所有线程都到达离自己最近的安全点后停顿下来:抢先式中断和主动式中断。

3.1.1 抢先式中断

GC发生时中断所有线程,如果有线程不在安全点上就恢复这些线程,让它们执行到安全点位置上。这种方案几乎没有虚拟机还在使用。

3.1.2 主动式中断

主动式中断不会直接操作线程,而是在GC发生时设置一个标记,当线程执行时会主动轮询该标志,如果是中断标志则自己中断并挂起,并且轮询标志的位置和安全点是重合的。

3.3 安全区域

安全点很好的解决了如何GC的问题,但是安全点针对的是活动的线程,有些特例是那些处理Sleep状态和Block状态的线程,它们无法去进行轮询中断标志,此时就引入了另一个概念—安全区域。

安全区域是指在一段代码中对象的引用关系都不会发生改变,在这个区域的任意位置开始GC都是安全的。当线程执行到安全区域时它会标识自己处于安全区域,当虚拟机发起GC时会忽略带有这些标识的线程。而当这些线程要离开安全区域时,会先检查虚拟机是否已经完成了GC Roots的枚举,如果完成了线程就继续执行,否则就会等待。