Java 垃圾回收 - 如何判断对象已死

关于 Java 的垃圾收集( Garbage Collection,GC ),Java 运行时区域的各个部分,程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不絮的执行者岀栈和入栈操作。
每个栈帧中分配多少内存基本上实在类结构确定下来时就已知的,因此在这几个区域内存的分配和回收都具有确定性,就不需要过多考虑回收的问题,因为在方法结束或者线程结束时,内存就自然跟着回收了。既然 Java 的内存回收已经如此智能,我们为什么还有继续了解 GC 和内存分配呢?

答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。做好垃圾收集,我们不得不完成 3 件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

对象已死吗

引用计数算法

引用计数算法( Reference Counting )是这样的:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1 ;当引用失效时,计数器值就减 1 ;任何时刻计数器值为 0 的对象就是不可能再被使用的。
这种算法实现简单,判定效率也比较高,在大部分情况都是一个不错的算法,但是主流的 Java 虚拟机里没有选用它来管理内存,其中最主要的原因是他很难解决对象之间相互循环引用的问题。

请看下面例子,两个对象 o1、o2 除了它们之间的相互引用外并无其他引用,实际上这两个对象已经不可能在被访问,但是由于他们之间的相互引用,导致它们的引用计数器都不为 0 ,于是引用计数算法无法通知 GC 收集器回收他们。

public class ReferCount {

  public static void main(String[] args) {
    BigObj o1 = new BigObj();
    BigObj o2 = new BigObj();

    o1.bigObj = o2;
    o2.bigObj = o1;

    System.out.println("----- gc start 1 ------");
    System.gc();
    System.out.println("----- gc end 1 ------");

    o1 = null;
    o2 = null;

    System.out.println("----- gc start 2 ------");
    System.gc();
    System.out.println("----- gc end 2 ------");
  }

}

class BigObj {
  public BigObj bigObj;
  /* 初始化一个 byte 数组,主要是为了占点内存 */
  private byte[] bigSize = new byte[5 * 1024 * 1024];
}

使用 java -XX:+PrintGC ReferCount 运行上面程序,控制台输出:

----- gc start 1 ------
[GC (System.gc())  12820K->10712K(245760K), 0.0034308 secs]
[Full GC (System.gc())  10712K->10550K(245760K), 0.0061232 secs]
----- gc end 1 ------
----- gc start 2 ------
[GC (System.gc())  11840K->10614K(245760K), 0.0008369 secs]
[Full GC (System.gc())  10614K->310K(245760K), 0.0046662 secs]
----- gc end 2 ------

第一次回收打印的 10712K->10550K 说明我们初始化的数组确实占用了内存,两个对象都没有被回收,第二次回收打印的 10614K->310K 回收了 10304K ,也就是大约 o1、o2 中数组的大小。由此说明虚拟机并没有因为它们的相互引用而不回收它们,这也证明了虚拟机没有采用引用计数算法来判断对象是否存活。

可达性分析算法

在主流的商用程序语言( Java、C#、Lisp )的主流实现中,都是通过可达性分析( Reachability Analysis ) 来判定对象是否存活的。这个算法的基本思想就是通过一系列成为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径
成为引用链( Reference Chain ),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。如下图所示,对象 Object 5、Object 6、Object 7 虽然有关联,但是他们到 GC Roots 是不可达的,所以他们将被判定为是可回收的对象。

在 Java 语言中,可作为 GC Roots 的对象包括下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象

说明

文章摘自《深入理解Java虚拟机》第二版 周志明著,仅作为学习记录,书籍中用到的案例代码及描述有部分修改,但未改变原意。