跳至主要內容

GC|对象存活与否

AruNi_LuJavaJVM约 1855 字大约 6 分钟

本文内容

1. GC 需要回收什么?

我们知道,Java 内存运行时区域open in new window 分为线程私有和线程共享两部分:

  • 线程私有:程序计数器、Java 栈、本地方法栈;
  • 线程共享:堆、方法区。

对于 线程私有 的内存区域都是 随线程而生,随线程而灭,因此无需过多考虑内存回收的问题。随着方法或者线程的结束,内存自然就回收掉了

但是对于 线程共享 的内存区域,有着很显著的不确定性,只有在运行时再能确定会创建哪些对象,而且创建出来的对象也不会自己消失,所以就需要 垃圾收集器来管理这部分内存

另外,对于方法区来说,由于回收的判定条件非常严苛,垃圾收回的性价比比较低,所以我们重点是看 Java 堆上的垃圾收回,尤其是在新生代中,一次垃圾回收通常能清理 70% 到 90% 的内存空间。

2. 判断对象存活与否

知道了要回收哪里的垃圾后,接着要搞清楚哪些对象可以被当成垃圾呢?这就涉及到常见的两种 判断对象是否存活的算法 了,它们分别是 引用计数法可达性分析算法

2.1 引用计数法

引用计数法 非常简单,只需要 在对象中添加一个引用计数器,当 该对象被引用时,计数器就加一;引用取消时,计数器就减一。这样在 计数器为零的时候,该对象就是不会被使用了

引用计数法虽然占用了一些额外的内存空间来存放计数器,但它的原理简单,判定效率也高,所以在大多数情况下表现还不错。

但是,引用计数法有个很大的 缺点,就是会存在 对象之间循环引用 的问题,这也是现今的 JVM 都没有选用该方法的原因。

什么是循环引用呢?下面举一个简单的例子:

public class MyObj {
    public Object instance = null;
}

MyObj objA = new MyObj();
MyObj objB = new MyObj();
// 让 objA、objB 的 instance 变量互相引用对方
objA.instance = objB;
objB.instance = objA;

// 此时把 objA、objB 置为空
objA = null;
objB = null;

通过上面的实例可以发生,虽然最后把 objA、objB 都置为 null 了,但其实它们还被对方的 instance 变量所引用着,也就是各自的引用计数器都为 1,还不能被 GC 回收,但它们又都被直为 null 了,后面不会再使用了,这就造成了内存被白白的占用。

2.2 可达性分析算法

另一种常用的算法是 可达性分析算法,在该算法下,有一个 GC Root 集合,里面保存的都是一些 GC Root,这些 GC Root 会把一系列的引用关系连接起来,形成一条条的 引用链

这时 从 GC Root 的根对象作为起点,沿着引用链向下搜索,如果某个对象从 GC Root 不可达,也就是 该对象没有任何一条引用链可以到 GC Root,那么就能证明 该对象不会再被使用了

例如下面的 object5、object6 和 object7 都是不可达 GC Root 的,所以会被判定为可回收的对象。

image-20230820215119259

在 Java 中,可以作为 GC Root 的对象 主要包括以下几种:

  • 在 Java 栈中引用的对象
  • 在方法区中类静态变量引用的对象
  • 在方法区中常量引用的对象
  • 本地方法栈中 Native 方法引用的对象
  • 被同步锁(synchronized 关键字)持有的对象

3. finalize - 最后的援救

现在常用的 JVM 都是使用的可达性分析算法来判定对象存活与否,那么当一个对象被判定为不可达对象时,它一定会被下一轮 GC 消灭吗?

其实,它们还有一次被援救的机会,那就是 finalize() 方法,这是 Object 类的一个方法,也就是说,所有的类都会继承这个方法。

当一个对象被标记为不可达对象后,会进行一轮检查,看该对象是否要执行 finalize() 方法,检查手段是:

  • 该对象是否重写了父类 Object 的 finalize() 方法

  • finalize() 方法是否已经被执行过了

    因为 任何一个对象的 finalize() 方法都只会被系统自动调用一次

如果确定了该对象要执行 finalize() 方法,则会把该对象放入一个 F-Queue 队列中,之后会由一个叫做 Finalizer 线程去执行它们的 finalize() 方法。

当然了,为了避免执行 finalize() 方法的时间过长、或者发生死循环,所以并不会等该方法执行完毕,而是会有个时间限制。

在执行 finalize() 方法时,如果对象在该方法中成功拯救了自己,即 重新与引用链上的任何一个对象建立关联,那么该对象就会被移出回收集合中,从而 拯救成功

下面给出一个援救的例子:

public class FinalizeEscapeGC {
  
    public static FinalizeEscapeGC SAVE_HOOK = null;
  
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;	// 自救
    }
  
	public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
    
        // 对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为 Finalizer 优先级较低,暂停 0.5 秒,等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
        	System.out.println("yes, i am still alive :)");
        } else {
        	System.out.println("no, i am dead :(");
        }

        // 这次自救却失败了,因为系统只会自动执行一个 finalize 方法
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
        	System.out.println("yes, i am still alive :)");
        } else {
        	System.out.println("no, i am dead :(");
        }
	}
}

输出:

finalize method executed!
yes, i am still alive :)
no, i am dead :(

不过并 不推荐使用 finalize() 方法来拯救对象,因为该方法的 运行代价高、不一定会被执行完毕、无法确定各对象的调用顺序,官方也不推荐使用该方法。

4. 总结

垃圾收集器主要是回收 Java 堆中一些不使用的对象,要知道哪些对象是不会使用的,就涉及到两种对象判活算法:

  • 引用计数法;
  • 可达性分析算法。

由于 引用计数法存在循环引用的问题,会导致不使用对象占用的内存空间无法被回收,因此通常都是使用可达性分析算法。

可达性分析算法 中,存在着一些 GC Root,要判断该对象是否可被回收,只需看 该对象是否存在一条可达 GC Root 的引用链 即可。

最后提到了 finalize 方法,它虽然 可以拯救不可达 GC Root 的对象,但由于它的 执行代价高、不确定性大,所以也不推荐使用了。

上次编辑于: