【JVM】四、深入理解JVM的垃圾回收原理

在上一篇文章中,我们已经详细分析了JVM中的几个关键内存区域以及它们各自的功能。今天,我们将深入探讨垃圾回收的概念。

1、前文回顾

在上一篇文章中,我们已经详细分析了JVM中的几个关键内存区域以及它们各自的功能。今天,我们将深入探讨垃圾回收的概念。

首先,让我们回顾一下昨天的图表,以便重新理解JVM中各个内存区域的作用。

在理解JVM的运行原理时,我们需要在脑海中构建一个动态的图景。首先,当我们的代码开始运行时,至少会有一个主线程(main thread)负责执行大部分的代码,当然,这也可能是由你主动启动的其他线程。

接下来,每个线程在执行时,都会通过其自身的程序计数器来跟踪和记录它已经执行到哪一条代码指令。这个程序计数器是线程私有的,每个线程都有一个独立的程序计数器。

再者,当线程需要执行某个方法时,它会为该方法创建一个新的栈帧(stack frame),并将这个栈帧放入自己的Java虚拟机栈中。这个栈帧包含了该方法的局部变量,以及用于恢复方法调用状态的其他信息。

最后,我们的代码在运行过程中可能会创建许多对象,这些对象都会被存储在Java堆内存中。这是所有线程共享的内存区域,用于存储Java对象实例。

通过结合以上各个部分,我们可以更好地理解JVM的运行原理。现在,大家应该对JVM的工作原理有了一个初步的理解,并能够把握其核心概念。

2、对象的分配与引用

在当前的上下文中,我们假设有一段代码,其基本功能是通过执行loadUserInfosFromDB方法来从硬盘上加载所需的副本数据。然后,通过创建的UserManager对象实例来完成此操作。代码如下所示:

public class User {
    public static void main() {
        loadUserInfosFromDB();
    }
    private static void loadUserInfosFromDB() {
        UserManager UserManager = new UserManager();
        UserManager.load();
    }
}

结合我们之前了解过的JVM运行原理,让我们通过动态的图来详细解析上述代码的运行流程。

首先,一个main线程会被创建并执行main()方法中的代码。这个main线程拥有自己的Java虚拟机栈,它会将main()方法的栈帧压入Java虚拟机栈中。如下图所示:

image-20240411084221514

在main()方法中,我们调用了loadUserInfosFromDB()方法。这导致Java虚拟机为loadUserInfosFromDB()方法创建了一个栈帧,并将其压入main线程的堆栈。这个过程如下图:

image-20240418160435954

loadUserInfosFromDB()方法中,发现了一个名为userManager的变量。因此,在该方法对应的栈帧中,也会创建一个名为userManager的变量。继续看下图:

image-20240418160542277

在代码中,我们创建了一个名为“UserManager”的类的实例对象。当这个实例对象被创建时,Java的堆内存会为这个实例对象分配相应的内存空间。

同时,我们让loadUserInfosFromDB()方法的栈帧中的“UserManager”局部变量指向了那个位于Java堆内存中的UserManager实例对象。这样,我们就可以通过“UserManager”这个局部变量来操作和访问UserManager实例对象,从而完成我们的业务逻辑。大家看下图:

image-20240418160747079

接下来,我们将通过“UserManager”这个局部变量所引用的“UserManager”实例对象来执行他的load()方法,以实现我们所需的业务逻辑。

好,到目前为止,我们所讲解的内容其实都是之前上篇文章中已经介绍过的知识,我们只是重新进行了一遍梳理和串联,这部分内容应该都非常容易理解。

3、这个方法后会发生什么?

接着大家来回顾一下上面的代码。

public class User {
    public static void main() {
        loadUserInfosFromDB();
    }
    private static void loadUserInfosFromDB() {
        UserManager UserManager = new UserManager();
        UserManager.load();
    }
}

在目前的讨论中,我们已经详细分析了代码,并聚焦到了"UserManager.load()"这行代码。接下来,我们可能会产生一个疑问:如果这行代码执行完毕之后,会发生什么情况呢?

首先,我们需要回顾一下之前的内容。我们曾经提到过,一旦方法中的代码全部被执行完毕,那么该方法的执行也就结束了。换句话说,当"UserManager.load()"代码行执行完毕后,"loadUserInfosFromDB()"方法也就会执行完毕。

然后,当我们的"loadUserInfosFromDB()"方法执行完毕时,Java虚拟机会将与"loadUserInfosFromDB()"方法对应的栈帧从主线程(main thread)的Java虚拟机栈中进行出栈操作。如下图所示:

image-20240418160841551

一旦loadUserInfosFromDB()方法的栈帧被弹出,您会发现该栈帧中的局部变量“UserManager”也随之消失。这意味着,没有任何变量再指向Java堆内存中的“UserManager”实例对象。

4、对象创建与内存消耗的那些事儿

此时大家发现了,在Java的堆内存中,有一个名为“UserManager”的实例对象,现在已经没有任何引用指向它了。这个对象已经完成了它的任务,现在再让它留在内存中就没有意义了。

我们需要明白,内存资源是有限的。通常,我们会在一台机器上启动一个Java系统,而这台机器的内存资源是有限的,比如只有4GB的内存。我们启动的Java系统实际上是一个JVM(Java虚拟机)进程,这个进程负责运行我们的系统代码,这已经在之前的解释中提及过了。

这个JVM进程本身会占用机器上的一部分内存资源,比如2GB的内存。我们在JVM的Java堆内存中创建的对象,实际上也会占用JVM的内存资源,比如“UserManager”实例对象,它会占用500字节的内存。

因此,我们应该清楚地认识到一个核心点:我们在Java堆内存中创建的对象,都是会占用内存资源的,而且内存资源是有限的。

大家看下面的图,感受会深一点。

image-20240418161013549

5、如何处理不再需要的对象?

在深入分析上述问题时,我们注意到“UserManager”对象实例已经变得不再必要,因为它没有任何方法的局部变量正在引用它。此外,它仍然占用着宝贵的内存资源。那么,我们应该如何处理这种情况呢?

解决此问题的关键就在于JVM的垃圾回收机制。JVM内置了一个自动运行的垃圾回收线程。只要你启动一个JVM进程,这个垃圾回收线程就会自动运行。

这个垃圾回收线程的主要职责是在后台持续检查JVM堆内存中的各个实例对象。如果发现某个实例对象不再被任何活跃的局部变量引用,且因此无法再被程序访问,垃圾回收线程就会将其标记为垃圾,并在适当的时机进行清理,从而释放其占用的内存资源。

因此,对于不再需要的“UserManager”对象实例,我们可以依赖JVM的垃圾回收机制来处理。这样,我们就无需手动进行内存管理,可以更加专注于编写业务逻辑代码。还是给大家画一张图,来看看这个过程:

image-20240418161247124

如果一个实例对象没有被任何方法的局部变量引用,也没有被任何类的静态变量(包括常量等)引用,那么垃圾回收线程会将其视为不再需要的“UserManager”实例对象,并将其从内存中清除,释放其所占用的内存资源。

这样一来,这些不再被引用的对象实例,即在JVM中的“垃圾”,会被后台垃圾回收线程定期清理,从而不断释放内存资源。大家看下图:

image-20240411090042490

6、本文小结

在阅读了本文的详细解析之后,相信诸位读者已经对文章的主题和脉络有了一个清晰的认识。现在,让我们来回顾并梳理一下关键概念。

首先,我们探讨了JVM(Java虚拟机)中所谓的“垃圾”。在JVM的上下文中,“垃圾”是指那些不再被程序引用,因此无法再被使用的对象。这些对象占用了宝贵的内存资源,如果不被及时清理,就会导致内存泄漏,进而影响系统性能。

其次,我们解释了JVM中的“垃圾回收”(Garbage Collection),这是一个自动的内存管理过程。垃圾回收的主要任务是识别出那些不再被程序引用的对象,并回收它们所占用的内存空间,以便这部分内存可以被重新分配和使用。这个过程对于维持应用程序的性能和稳定性至关重要。

总结来说,JVM中的“垃圾”指的是无用的对象,而“垃圾回收”则是JVM自动清理这些无用对象,释放内存资源的机制。通过本文的阐述,希望读者们对这些概念有了更深入的理解。