目录
1. 什么是垃圾回收器?
关于垃圾回收器第一印象正如它的名字所暗示的——寻扎垃圾并且丢弃它们。但实际上它并不是这样的。垃圾回收器追踪所有正在使用的对象,并将其余的对象标记为垃圾。考虑到这一点,我们开始深入探究被称为“垃圾回收”的内存自动回收机制在Java虚拟机中实现细节。
别猴急,我们先从垃圾回收器的核心概念和方法以及它所拥有的性质开始讲解。
2. 手动内存管理
在我们开始介绍垃圾回收器的现状之前,让我们一起快速回顾一下那个还需要手动释放申请/释放内存的岁月。如果你忘记了释放它,你就无法再次使用这片内存。(作者想表达的意思应该是:如果开发者没有精确掌控这片内存区域,那么下次对这片内存区域的操作得到的期望值将不可控)。这片内存可以被声明,但是不能被使用。这种情况被称为内存泄露。
这是一个用C语言编写的手动管理内存的示例:
int send_request() {
size_t n = read_size();
int *elements = malloc(n * sizeof(int));
if(read_elements(n, elements) < n) {
// elements 没有被释放!
return -1;
}
// …
free(elements)
return 0;
}
我们可以看到,忘记释放内存是常常发生的事情。与今天相比,内存泄漏在以往是更为常见的问题。你只能通过修复代码来修复它们。因此,更好的方法是自动回收未使用的内存,完全消除人为疏忽导致错误的可能性。这种自动化机制称为垃圾收集(简称GC)。
2.1 智能指针
第一种自动化的方法之一是使用析构函数。例如,我们可以使用vector在C++中做同样的事情,当它不再在函数作用域范围内时,它的析构函数将被自动调用:
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++的共享指针:
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。它会跟踪并持有该vector对象的引用计数。每使用它一次引用计数就会+1,而当它离开作用域时引用计数会-1。一旦引用计数变成0,shared_ptr 就会自动删除这个vector对象。
3. 自动内存管理
在上面的C/C++代码中,虽然我们仍需要明确行内存管理的时机,但是如果我们能够使所有对象都以这种方式运行将非常方便。因为开发人员不再需要亲自考虑这些对象的清理时机。运行时系统将自动了解哪些内存不再使用并释放它。换句话说,它会自动清理垃圾。第一个垃圾回收器是在1959年为Lisp创建的,从那时起该技术才得以发展。
3.1 引用计数
我们已经论证了用C++的智能指针的想法可以应用于所有对象。许多语言,如Perl,Python或PHP都采用这种方法。如图所示:
绿色云表示他们指向的对象仍然由程序员使用。从技术上讲,这些可能是当前正在执行的方法中的局部变量或静态变量或其他内容。它可能因编程语言而异,因此我们不会在此处关注它。
蓝色圆圈是内存中的活动对象,其中的数字表示其引用计数(这些对象由绿色云直接引用)。最后,灰色圆圈是未从任何仍明确使用的对象引用的对象(绿色、蓝色对象都未引用灰色对象)。因此灰色对象是垃圾,可以由垃圾收集器清理。
这一切看起来都很好,不是吗?嗯,确实如此,但整个方法都有很大的缺点。由于循环引用,它们的引用计数不为零,因此很容易最终得到一个分离的对象环使得这些对象都不在回收范围内。红色圆环这是一个例子:
红色对象实际上是应用程序不使用的垃圾。但由于引用计数的限制,仍然存在内存泄漏。
有一些方法可以避免这个问题,例如使用特殊的“弱”引用或利用其他独立的算法来收集这种圈(红色环)。上述语言 - Perl,Python和PHP - 都以某种方式处理这种环,但这超出了本手册的范围。取而代之的是我们将开始详细研究JVM所采用的方法。
4. 标记和扫描
首先,JVM更为具体地描述了对象的可达性。我们有一个非常具体和明确的对象集,称为Garbage Collection Roots,而不是上述章节中的绿色云朵。
- 局部变量
- 活动线程
- 静态字段
- JNI引用
JVM中使用的标记-扫描算法用于跟踪所有可达的存活对象,并确保不可达对象开辟的内存可以重用。它包括两个步骤:
- 标记:从GC根开始访问所有可达对象,并在本地内存中记录下关于这些对象的信息。
- 扫描:确保不可到达对象占用的内存空间可以在之后的分配中得到重用。
JVM中的不同GC算法(例如Parallel Scavenge,Parallel Mark+Copy或CMS)的做法稍有不同。但从原理上讲仍然与上述两个步骤相仿。
该方法的重中之重是在于保证环路(循环依赖)不再发生内存泄漏:
上述方法的缺陷是在内存回收时需要将所有的应用线程暂停住。因为不断变化的内存变化使得引用计算难以实施。当应用程序暂时(业务逻辑)执行以便JVM可以专注于垃圾回收时,这种情况称为"Stop The World"。多种原因可能导致STW事件发生,但垃圾收集是迄今为止最受亲耐的一种。