Java垃圾收集机制详解
乍一看,垃圾收集的作用正如其名称所暗示的那样——查找并删除垃圾。 事实上恰恰相反。 垃圾收集跟踪所有仍在使用的对象,然后将剩余的对象标记为垃圾。 考虑到这一点,让我们深入了解一下这种称为“垃圾收集”的自动内存回收在 JVM 中是如何实现的。
手动管理内存
在介绍现代版本的垃圾收集之前,让我们简单回顾一下手动显式分配和释放内存的日子。 如果忘记释放内存,则内存无法重复使用。 这块内存被占用但没有使用。 这种情况称为内存泄漏。
下面是一个用 C 语言编写的手动内存管理的简单示例:
int send_request() { size_t n = read_size(); int *elements = malloc(n * sizeof(int)); if(read_elements(n, elements) < n) { // elements not freed! return -1; } // … free(elements) return 0; }
正如您所看到的,您很容易忘记释放内存。 内存泄漏曾经是一个非常常见的问题。 你只能通过不断修复自己的代码来对抗它们。 因此,需要有一种更优雅的方式来自动释放无用的内存,以减少人为错误的可能性。 这个自动化过程也称为垃圾收集(简称GC)。
智能指针
自动垃圾收集的早期实现是引用计数。 您知道每个对象被引用了多少次,当计数器达到零时,该对象就可以被安全回收。 C++的共享指针是一个非常著名的例子:
int send_request() { size_t n = read_size(); stared_ptr> elements = make_shared >(); if(read_elements(n, elements) < n) { return -1; } return 0; }
我们使用的会记录这个对象被引用的次数。 如果传递计数,计数就会加一;如果超出范围,计数就会减一。 一旦该计数达到 0,底层对应项将被自动删除。 当然,这只是一个例子,因为有读者指出这在现实中不太可能发生,但作为演示已经足够了。
自动内存管理
在上面的C++代码中,我们还必须明确声明我们需要使用内存管理。 那么如果所有对象都使用这个机制呢? 这实在是太方便了,开发者不需要考虑清理内存的事情。 运行时会自动知道哪些内存不再使用,然后释放它。 换句话说,它会自动回收垃圾。 第一代垃圾收集器于 1959 年在 Lisp 中引入,此后该技术不断发展。
引用计数
我们刚刚用 C++ 共享指针演示的思想可以应用于所有对象。 Perl、PHP等很多语言都使用这种方法。 这可以很容易地用一张图来解释:
绿色云代表程序中仍在使用的对象。 从技术上讲,这有点像正在执行的方法中的局部变量,或者静态变量。 对于不同的编程语言情况可能会有所不同,所以这不是我们关注的重点。
蓝色圆圈代表内存中的对象,您可以看到有多少对象引用它们。 灰色圈出的对象不再被任何人引用。 因此,它们是垃圾对象,可以被垃圾收集器清理掉。
看起来不错,对吧? 确实如此,但这里有一个重大缺陷。 很容易出现一些孤立的环,其中的对象不在任何域中,但相互引用,因此引用计数不为 0。下面是一个示例:
你看,红色部分实际上是应用程序不再使用的垃圾对象。 由于引用计数的缺陷,会出现内存泄漏。
有几种方法可以解决这个问题,例如使用特殊的“弱”引用,或者使用特殊的算法来回收循环引用。 前面提到的Perl,以及PHP等语言,都使用类似的方法来恢复循环引用,但这超出了本文的范围。 我们将详细介绍 JVM 所采用的方法。
标记为删除
首先,JVM对对象可达性有更清晰的定义。 它不再像之前绿云一样模糊,而是对垃圾收集根对象(Roots)有非常明确和具体的定义:
JVM通过标记-删除算法记录所有可达(存活)对象,同时保证不可达对象的内存可以被重用。 这涉及两个步骤:
JVM中不同的GC算法,例如Mark+Copy、CMS都是该算法的不同实现,但阶段略有不同,概念上仍然对应于上面提到的两个步骤。
这个实现最重要的是不会再有泄漏的对象环:
缺点是需要挂起应用程序线程来完成回收,并且如果引用一直在变化,则无法计数。 这种应用程序暂停以便 JVM 可以进行清理的情况也称为 Stop The World 暂停 (STW)。 触发这种暂停的可能性有很多种,但垃圾收集应该是最常见的一种。
感谢您的阅读,希望对您有所帮助,感谢您对本站的支持!