Java垃圾回收手册翻译 - 什么是垃圾回收
初看之下,垃圾回收应该要做其名称之事 - 找到和丢掉垃圾。然而事实上它正好做着相反的事,垃圾回收会记录所有仍在使用中的对象,然后将其他标记为垃圾。谨记这点,我们开始挖掘更多Java虚拟机如何实现被称为垃圾回收的自动化内存回收过程的细节。
为了避免一头扎进细节,我们从头开始,解释垃圾回收的一般性质以及核心概念和方法。
免责声明:本手册关注于Oracle Hotspot和OpenJDK的表现,其他运行时环境甚至其他虚拟机,比如jRockit或IBM J9,会在某些方面有不同于本手册所涵盖的表现。
手动内存管理
在开始现代形式的垃圾回收内容之前,我们先快速回顾一下那些需要手动显示分配和释放数据内存的日子。那时候如果你忘记释放内存,你将不能重用那块内存。这部分内存会被声明到但是没有使用,这种场景叫做内存泄漏。
下面是一个用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++中的向量vector做上一个例子中同样的事,它会在离开其作用域时自动的调用其析构函数:
int send_request() {
size_t n = read_size();
vector<int> elements = vector<int>(n);
if(read_elements(elements.size(), &elements[0]) < n) {
return -1;
}
return 0;
}
但是再更复杂的情况中,尤其是在多个线程中共享对象时,仅仅使用析构函数并不够。这就引出了垃圾回收最简单的形式:引用计数。对每个对象,你只需要知道它有多少次被引用到,并且当这个计数达到零时这个对象就可以安全的被回收。一个众所周知的例子是C++中的共享指针shared pointers:
int send_request() {
size_t n = read_size();
auto elements = make_shared<vector<int>>();
// read elements
store_in_cache(elements);
// process elements further
return 0;
}
现在,为了避免下次函数调用时再去读这个元素elements,我们可能想要缓存他们。在这个例子中,就不能再当向量vector离开作用域时销毁它。因此,我们使用共享指针shared_ptr,它记录引用到它的数量,当你使用它传值时计数增加,同时当它离开作用域时计数减少。一旦引用的计数降到零,共享指针shared_ptr就会自动地删除下面的向量。
自动化内存管理
在上面的C++代码中,我们仍然要显式地指出我们要何时关心内存管理。但是如果我们能让所有对象都按这种方式运行呢?那将会十分方便,因为开发者不再需要在用完后考虑清理工作。运行时环境会自动知道某些内存不再使用,然后进行清理,也就是自动回收垃圾。最早的垃圾收集器是在1959年为Listp语言创建的,从那时起这项技术才开始不断前进。
引用计数
我们上面演示的C++中共享指针的想法可以应用到所有对象。许多编程语言,比如Perl、Python和PHP都采用了这个方法。下面一张图片可以很好地阐述它:
绿色的云朵表明它们指向的对象仍在被程序员使用。严格的说,它们可能是当前执行方法中的本地变量或静态变量等。这会随着不同的编程语言而不同,所以我们这里先不关注它。
蓝色的圆圈是内存中活着的对象,其中的数字表示它们被引用的次数。最后,灰色的圆圈是没有被明确使用中的对象(那些直接被绿色云朵引用到的)所引用到的对象。这些灰色的对象是就是垃圾,可以被垃圾收集器清理。
这看起来真的很好,不是么?好吧,确实如此,但是整个方法有个巨大的缺陷。它很容易被一些对象形成的分离的循环终结,这些对象都已不在使用范围但是由于循环引用,因此它们的引用计数都不是零。举例来说:
看到么?红色的对象实际上是程序不再使用的垃圾。但是由于引用计数的限制,仍然有内存泄漏。
有几种办法可以克服这个缺陷,比如使用特殊的'弱引用'或对循环使用单独的算法。前面提到的编程语言-Perl、Python和PHP,都用了一种或另一种方法处理循环,这超出了本手册的范围。相反,我们开始更详细地研究JVM所使用的方法。
标记和清除
首先,JVM对于对象的可达性构成更加具体。与我们之前看到绿色云朵的含糊定义相反,我们有一个非常详细和明确的一组对象,被称为是垃圾回收根源(GC Roots)。
- 本地变量
- 活动线程
- 静态字段
- JNI引用
JVM用来追踪所有可达(活着)的对象和确保不可达对象使用的内存能被重用的方法被称为标记和清除算法。它由两个步骤构成:
- 标记阶段 遍历所有可达对象,从垃圾回收根源开始并在本地内存中记录一个所有这些对象的分类账本。
- 清除阶段 确保不可达对象使用的内存可以在下次内存分配时重用。
JVM中不同的垃圾回收算法,比如并行清扫、并行标记+复制或CMS 在实现这些阶段时略有不同,但是从概念上都有着同上述两个步骤相似的过程。
该关方法的一个至关重要的情况是循环不再产生内存泄露:
不太好的方面是情况是应用线程需要在回收发生时暂停,因为如果引用在不停的变化,你将无法真正的计算出引用的数量。这种临时暂停应用程序以便于JVM专心做清理工作的情况被称为停止世界暂停。它们会因很多原因产生,但是垃圾收集是至今最普遍的一个。