导读:乍一看,垃圾收集应该处理顾名思义的问题-查找并丢弃垃圾。实际上,它所做的恰恰相反。垃圾收集正在跟踪所有仍在使用的对象,并将其余对象标记为垃圾。牢记这一点,我们开始深入研究如何为Java虚拟机实现称为“垃圾回收”的自动内存回收过程的更多细节。
我们从头开始,不着急于细节,而是说明垃圾收集的一般性质以及核心概念和方法。
在我们开始以现代形式介绍Garbage Collection之前,让我们快速回顾一下您不得不手动和显式分配和释放数据存储空间的日子。而且,如果您忘记释放它,则将无法重用该内存。该内存将被声明但未被使用。这种情况称为内存泄漏。
这是一个使用C语言编写的,使用手动内存管理的简单示例:
如我们所见,忘记释放内存是很容易的。内存泄漏曾经是比今天更常见的问题。您只能通过修复代码来真正打败他们。因此,更好的方法将是自动回收未使用的内存,从而完全消除人为错误的可能性。这种自动化称为垃圾收集(或简称GC)。
自动执行此操作的第一种方法是使用析构函数。例如,我们可以在C ++中使用vector进行相同的操作,当它的作用域不再在作用域内时,其析构函数将被自动调用:
但是在更复杂的情况下,尤其是在多个线程之间共享对象时,仅析构函数是不够的。垃圾收集的最简单形式是:引用计数。对于每个对象,您只需知道它被引用了多少次,并且当计数达到零时,就可以安全地回收该对象。一个众所周知的例子是C ++的共享指针:
现在,为避免下次调用该函数时读取元素,我们可能需要对其进行缓存。在这种情况下,当超出范围时销毁向量是不可行的。因此,我们使用 shared_ptr。它跟踪对它的引用数。此数字随着您的传递而增加,而随着其离开范围而减小。一旦引用数达到零, shared_ptr就会 自动删除基础向量。
在上面的C ++代码中,我们仍然必须明确地说出何时需要进行内存管理。但是,如果我们可以使 所有 对象以这种方式表现呢?这将非常方便,因为开发人员不再需要考虑自己清理。运行时将自动了解不再使用某些内存并将其释放。换句话说,它会自动 收集垃圾。第一个垃圾收集器是在1959年为Lisp创建的,此后技术才有所发展。
我们用C ++的共享指针演示的想法可以应用于所有对象。许多语言(例如Perl,Python或PHP)都采用这种方法。最好用图片来说明:
绿云表明程序员指向的对象仍在使用中。从技术上讲,这些可能是诸如当前正在执行的方法中的局部变量或静态变量之类的东西。它可能因编程语言而异,因此在此我们将不再关注。
蓝色圆圈是内存中的活动对象,其中的数字表示其引用计数。最后,灰色圆圈是未从仍在显式使用的任何对象中引用的对象(这些对象由绿云直接引用)。因此,灰色物体是垃圾,可以由垃圾收集器清理。
这一切看起来真的很好,不是吗?可以,但是整个方法都有很大的缺点。结束 对象的分离循环是很容易的 ,这些对象都不在范围内,但是由于循环引用,其引用的计数不为零。这是一个例子:
看?红色对象实际上是应用程序不使用的垃圾。但是由于引用计数的限制,仍然存在内存泄漏。
有一些方法可以克服此问题,例如使用特殊的“弱”引用或应用单独的算法来收集周期。前面提到的语言(Perl,Python和PHP)都以一种或另一种方式处理循环,但这超出了本手册的范围。相反,我们将开始更详细地研究JVM所采用的方法。
首先,JVM更具体地说明了对象的可访问性。除了我们在前面的章节中看到的模糊定义的绿色云之外,我们还有一组非常具体和明确的对象,称为“垃圾收集根”:
JVM用于跟踪所有可达(活动)对象并确保可以重用非可达对象声明的内存的方法称为标记和清除算法。它包括两个步骤:
JVM中的不同GC算法,例如Parallel Scavenge,Parallel Mark + Copy或CMS,在实现这些阶段时略有不同,但是在概念上,该过程仍然类似于上述两个步骤。
关于此方法,至关重要的一点是周期不再泄漏:
不太好的事情是,需要停止应用程序线程以进行收集,因为如果引用一直在变化,那么您就无法真正计数引用。当应用程序暂时停止以使JVM可以沉迷于家政活动时,这种情况称为Stop The World暂停。它们的发生可能有多种原因,但是垃圾收集是迄今为止最受欢迎的一种。