Java 垃圾收集

在 Java 中,运行时内存区域可以分为:程序计数器、虚拟机栈、本地方法栈、堆和方法区,其中程序计数器、虚拟机栈和本地方法栈是线程私有的,因此这部分划分的内存与线程的生命周期相同:栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,每一个栈帧中分配多少的内存基本上在类结构确定之后就已知了(在概念模型中可以暂时忽略 JIT 编译器的优化),因此这几个区域的内存分配和回收都具有确定性。而 Java 堆和方法区则不一样,一个接口的多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存也可能不一样,只有在代码处于运行期间才能知道会创建哪些内存,这部分内存的分配和回收都是动态的,Java 的 GC 关注的就是这部分内存。

垃圾收集需要完成三件事情:首先需要确定哪些内存需要回收,然后是何时回收以及怎样回收。

对象已死吗(哪些需要回收)

垃圾收集器在对堆进行回收之前,需要先确认哪些对象还“存活”着,哪些已经“死去”。而判断对象是否存活的关键就是引用。

引用

在 JDK 1.2 以前,引用的定义很传统,如果变量中存储的是另外一块内存的起始地址,那么这块内存就代表着一个引用。在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用

  • 强引用(StrongReference)就是类似 Object obj = new Object() 这类的引用,只要强引用还存在,垃圾收集器就永远不会回收被引用的对象。
  • 软引用(SoftReference)用来描述一些有用但非必须的对象。只有在系统将要发生内存溢出异常之前,才会对软引用关联的对象进行回收,如果这次回收后还没有足够的内存,才会抛出 OutOfMemoryError。
  • 弱引用(WeakReference)的强度比软引用更弱,被弱引用关联的对象只能生存到下次垃圾收集之前。
  • 虚引用(PhantomReference)是最弱的一种引用,虚引用顾名思义,就是形同虚设。虚引用并不会影响对象的生命周期,如果一个对象仅关联着虚引用,那么它就和没有任何引用一样,在任何时候都可能会被 GC 回收,我们无法通过虚引用获取它所关联的实例对象。虚引用的唯一作用就是跟踪垃圾收集器的回收活动。并且需要注意的是,虚引用必须和引用队列(ReferenceQueue)配合使用,当垃圾收集器准备回收一个对象时,如果发现它还有虚引用,那么就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

引用计数算法

引用计数法就是给对象添加一个引用计数器,每当有一个地方引用它,计数器就加一;当引用失效时,计数器就减一。当计数器的值为 0 时表示对象没有被任何地方引用,可以进行回收。

引用计数器算法实现简单,效率也很高,但是在主流的 Java 虚拟机里都没有采用这种算法,主要原因是它很难解决对象之间相互循环引用的问题。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
public B b;
}

class B {
public A a;
}

public class ReferenceCounting {

public static void main(String[] args) {
A a = new A();
B b = new B();

a.b = b;
b.a = a;

a = null;
b = null;
}
}

虽然对象 a 和 b 都为 null,但是由于 a 和 b 之间存在循环引用,因此它们的引用计数器都不为 0,GC 也就一直无法回收它们。

可达性分析算法

可达性分析算法通过一系列的称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连,就可以说从 GC Roots 到这个对象不可达,也就意味着这个对象是可以被回收的。

可达性分析

在 Java 中,可以作为 GC Roots 的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象以及本地方法栈中 JNI(即 Native 方法)引用的对象。

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非一定会消亡,要真正回收一个对象,至少要经历两次标记过程。

在可达性分析后发现对象没有与 GC Roots 相连接的引用链,那么它会被进行第一次标记并进行一次筛选,当对象没有覆盖 finalize() 方法或者该对象的 finalize() 方法已经被虚拟机调用过,则虚拟机将认为该对象可以被回收;否则这个对象将会被放置在一个叫做 F-Queue 的队列中,稍后虚拟机会对该队列进行第二次小规模的标记:通过建立一个低优先级的 Finalizer 线程去执行队列中各个对象的 finalize() 方法。如果对象想要继续存活,就需要在覆盖的 finalize() 方法中重新与任意一个对象建立关联。

回收方法区

方法区(或者 HotSpot 虚拟机中的永久代)也存在垃圾收集,收集的主要内容为废弃常量无用的类

回收废弃常量与回收 Java 堆中的对象类似,以常量池中的字面量回收为例,当系统中没有任何一个 String 对象引用常量池中的某个常量,也没有其他地方引用了这个字面量,则在必要的时候此字面量就会被回收。

判断一个类是否是无用的类需要同时满足三个条件:该类所有的实例都已经被回收,加载该类的 ClassLoader 已经被回收,该类对应的 java.lang.Class 对象没有在任何地方被引用(包括反射)。在满足了这三个条件后,还需要虚拟机开启类回收功能,比如 HotSpot 虚拟机就提供了 -Xnoclassgc 参数。

在大量使用反射、动态代理、CGLIB 等字节码框架,动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具有类卸载的功能,以保证永久代不会溢出。

垃圾收集算法(如何回收)

垃圾收集算法的作用是指导收集器以何种方式回收内存,最基础的算法就是标记-清除算法,其他算法都是在它的基础上发展而来。

标记-清除算法

标记-清除(Mark-Sweep)算法是最基础的收集算法,该算法主要分为“标记”和“清除”两个阶段:标记阶段会标记出所有可以回收的对象,标记的过程在生存还是死亡中已经说明了。在标记完成后,虚拟机会统一进行回收。

该算法主要两个缺陷:一个是标记和清除的效率都不高;二是标记并清除之后会产生大量不连续的内存碎片,当碎片太多时可能会导致以后程序运行过程中需要分配大内存对象时因为无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

标记清除算法

复制算法

复制算法是将内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上,然后把之前使用的那一块内存空间一次清理掉。这样每次都是对整个半区进行内存回收,内存分配时也不必考虑碎片问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效,唯一的缺点就是可用内存缩小为原来的一半。

复制算法

本人水平有限,该文主要结合《深入理解 Java 虚拟机》和自己的理解整理自用,方便以后的以后深入学习。