【JVM】十、了解JVM判断对象可回收的神秘法则

本文介绍触发垃圾回收的时候,到底是按照一个什么样的规则来回收垃圾对象的

1、垃圾回收触发时机?

在我们之前的学习中,我们已经了解到,当我们的系统在运行过程中创建对象时,这些对象通常会被优先分配在所谓的“新生代”内存区域,如下图所示。

在新生代中,当对象数量逐渐增多,接近填满整个空间时,会触发垃圾回收机制。这个机制的作用是回收那些不再被引用的对象,从而释放内存空间。

需要特别注意的是,这是新生代垃圾回收的一个关键触发时机。如下图。

image-20240418162720839

那么本文就来针对这个过程,再次梳理其中的一些细节,看看触发垃圾回收的时候,到底是按照一个什么样的规则来回收垃圾对象的。

2、了解哪些引用导致对象无法被清理

首先第一个问题,一旦新生代快满了,那么垃圾回收的时候,到底哪些对象是能回收的,哪些对象是不能回收的呢?
这个问题非常好解释,JVM中使用了一种可达性分析算法来判定哪些对象是可以被回收的,哪些对象是不可以被回收的。
这个算法的意思,就是说对每个对象,都分析一下有谁在引用他,然后一层一层往上去判断,看是否有一个GC Roots。
这句话相当的抽象,是不是?

首先,我们来探讨一个关键问题:当新生代区域即将填满时,垃圾回收过程中,如何确定哪些对象应当被回收,哪些对象应当保留?

这个问题的解答相对直接。在JVM中,为了确定哪些对象是可回收的,哪些对象需要保留,采用了一种称为可达性分析的算法。

这个算法的核心思想在于,对每一个对象进行引用关系分析,逐层向上追溯,看是否存在一个名为GC Roots的起始点。如果一个对象可以从GC Roots通过引用链到达,那么这个对象就被认为是活动的,不会被垃圾回收;反之,如果无法从GC Roots通过引用链到达,那么这个对象就被视为垃圾,可以被回收。

这个概念可能听起来有些抽象,但实际上,它为垃圾回收提供了一个清晰且有效的判断机制。

不用担心,我们的特点就是一步一图,确保你能够理解。 举个例子,最常见的情况下,可以参照以下内容。

public class User {
    public static void main(String[] args) {
        loadUserInfosFromDB();
    }
    private static void loadUserInfosFromDB() {
        UserManager UserManager = new UserManager();
    }
}

在这段代码中,我们首先在一个方法内创建了一个对象,然后使用一个局部变量来引用该对象,这是非常常见的做法。

具体来说,当执行到“main()”方法时,会将其对应的栈帧压入栈中。接着,调用“loadUserInfosFromDB()”方法,该方法的栈帧也会被压入栈中。在这个过程中,我们会创建一个局部变量“UserManager”,并将其指向堆内存中的“UserManager”实例对象。

image-20240418160747079

在当前的上下文中,我们假设“UserManager”对象已经被一个局部变量引用。当新生代内存区域接近满载,触发垃圾回收(GC)时,垃圾回收器会进行可达性分析,以确定哪些对象是不再被使用的,因此可以回收。

在这个过程中,如果发现“UserManager”对象被引用,那么根据定义,这个对象就不能被回收。具体来说,它被局部变量“UserManager”引用。

在JVM规范中,局部变量被视为垃圾回收的根(GC Roots)。这意味着,只要一个对象被局部变量引用,它就具有一个GC Roots。因此,这个对象在垃圾回收过程中将被标记为“活跃”,并不会被回收。

另外比较常见的一个情况,其实就是类似下面的代码。

public class User {
    public static UserManager UserManager = new UserManager();
}

大家可以分析一下上面的代码,如下图所示。

image-20240418161735928

根据图示,我们可以思考一下垃圾回收的情况。假设我们有一个名为"UserManager"的对象,它被User类的一个静态变量"UserManager"所引用。

在JVM的规范中,静态变量被视为一种GC Roots(垃圾回收根节点)。当一个对象被GC Roots引用时,垃圾回收器不会对其进行回收。

因此,可以总结为一句话:只要一个对象被方法的局部变量或类的静态变量引用,垃圾回收器就不会回收该对象。

3、掌握Java对象的引用类型

在探讨引用和垃圾回收的关系时,我们需先理解Java中存在多种不同的引用类型。这些引用类型包括:强引用、软引用、弱引用以及虚引用。接下来,我将通过代码示例来分别展示这四种引用类型的使用。

强引用,就是类似下面的代码:

public class User {
    public static UserManager UserManager = new UserManager();
}

这个就是最普通的代码,一个变量引用一个对象,只要是强引用的类型,那么垃圾回收的时候绝对不会去回收这个对象的。

接着是软引用,类似下面的代码。

public class User {
    public static SoftReference<UserManager> UserManager = new SoftReference<UserManager>(new UserManager());
}

在这段文本中,我们将“UserManager”实例对象使用了一个“SoftReference”软引用类型的对象进行包裹。这样一来,变量“UserManager”对“UserManager”对象的引用就成为了一个软引用。在正常情况下,垃圾回收器是不会轻易地回收软引用对象的。然而,如果你在执行垃圾回收后发现可用的内存空间仍然不足以存放新的对象,并且内存几乎要溢出时,垃圾回收器将会选择回收这些被软引用关联的对象。尽管这些对象可能被变量引用,但由于它们与变量的关联是软引用,垃圾回收器还是会选择将它们回收,以释放内存空间。

接着是弱引用,类似下面的代码。

public class User {
    public static WeakReference<UserManager> UserManager = new WeakReference<UserManager>(new UserManager());
}

这个其实非常好理解,你可以将弱引用视为与没有引用相似。如果在垃圾回收过程中,弱引用的对象会被自动回收。

至于虚引用,实际上我们可以暂时忽略它,因为在实际应用中很少使用。

在这里,我们更常用的是强引用和软引用。强引用表示一个对象是绝对不能被回收的,而软引用则表示某些对象在内存不足时可以被回收。

4、finalize()方法究竟能做什么?

在理解了GC Roots和引用类型的概念之后,我们已经基本掌握了哪些对象可以回收,哪些对象不能被回收。

具体来说,如果一个对象被GC Roots引用,那么这个对象是不能被回收的。反之,如果没有被GC Roots引用,那么这个对象就可以被回收。然而,如果一个对象虽然被GC Roots引用,但是如果这种引用是软引用或者弱引用,那么这个对象还是有可能被回收的。

接下来,我们可能会有疑问,那就是对于没有被GC Roots引用的对象,它们是否一定会立即被回收呢?

实际上,答案是否定的。因为在Java中,有一个叫做finalize()的方法,它可以让一个对象有最后的机会去拯救自己,从而避免被垃圾回收器回收。这个方法会在垃圾收集器准备回收对象的内存之前被调用,如果在这个最后机会中,对象能够重新获得GC Roots的引用,那么它就可以避免被回收。看下面的代码。

public class UserManager {
    public static UserManager instance;
    
    @Override
    public void finalize() throws Throwable {
        UserManager.instance = this
    }
}

在Java中,当一个对象被标记为垃圾回收时,如果该对象重写了Object类中的finalize()方法,那么在垃圾回收之前,会先尝试调用这个对象的finalize()方法。这个方法的目的是给对象一个机会,让它在被垃圾回收前执行一些清理工作,比如释放资源、保存状态等。

在finalize()方法中,对象可以将自己的引用赋值给某个GC Roots变量,例如,可以把自己的引用赋值给类的静态变量。这样,这个对象就会重新被GC Roots引用,从而避免了被垃圾回收。

然而,这种做法并不常见,也并不是推荐的做法。因为这样会让垃圾回收器的工作原理变得更加复杂,可能会导致内存泄漏,或者让垃圾回收器无法及时回收不再使用的对象。所以,虽然Java提供了finalize()方法,但是在实际编程中,我们很少使用它。

总的来说,理解finalize()方法的工作原理是有用的,但是在大多数情况下,我们并不需要直接使用它。我们应该更多地依赖于其他机制,如try-with-resources语句,来确保资源的正确释放。