什么是垃圾收集?
乍一看,垃圾收集应该是顾名思义,即,找到并丢弃垃圾,但实际上却恰恰相反。实际上,垃圾收集是跟踪所有在用对象并标记非在用对象为垃圾。
我们将从源头出发,解释垃圾收集的一般性质、核心概念和方法,而不是匆忙地进入细节。
免责声明:本内容主要关注Oracle Hotspot和OpenJDK行为。在其他运行时,甚至在其他jvm(如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 没有被释放
return -1;
}
// …
// 释放elements
free(elements)
return 0;
}
正如上面我们所看到的一样,非常容易忘记释放内存。内存泄漏在过去是比现在更常见的问题,只能通过修改代码来对抗该问题。因此,更好的方法是自动回收不用的内存,完全消除人为错误的可能。这种自动化称为垃圾收集(Garbage Collection,简称GC)。
智能指针(Smart Pointers)
实现自动化的第一个方法是使用析构函数。例如,我们可以在C++中使用vector实现,当变量不在作用域中时,将自动调用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;
}
但是在更复杂的场景下,特别是跨多个线程共享对象时,仅仅使用析构函数是不够的。垃圾收集的最简单的形式是:引用计数(reference counting)。对于每个对象,你只知道它被引用了多少次,当其引用次数为零时,就可以安全地回收该对象。一个众所周知的例子是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;
}
现在,为了避免在下次调用函数时读取元素,我们可能想要缓存它们。在这样的场景下,不能在vector超出作用域时销毁它。因此,我们使用 shared_ptr,用它来记录对vector的引用次数。这个次数在传递vector时增加,在离开作用域时减少,一旦引用的次数达到零,shared_ptr就会自动删除基础的vector。
自动内存管理
在上面的C++代码中,我们仍然需要显性的指出何时进行内存管理。但如果我们能让所有对象都这样做的话,这将非常方便,因为开发人员不再需要考虑自己清理的工作。运行时将自动知晓哪些内存不再使用并释放它。也就是说,它会自动收集垃圾。第一个垃圾收集器是在1959年为Lisp创建的,从那时起,该技术才不断进步。
引用计数法(Reference Counting)
我们在C++的共享指针中演示的思想可以应用于所有对象。许多语言,如Perl、Python或PHP,都采用这种方法。下图是对该方法的说明:
GC ROOTS表示它们所指向的对象仍在使用中。从技术上讲,这些对象可能是当前执行的方法中的局部变量或者静态变量或者其他东西。它可能会因编程语言的不同而有所不同,此处不详细展开。
蓝色的圆是内存中存活的对象,圆里面的数字表示它们的引用次数。灰色的圆的对象没有被任何明确在用的对象引用(明确在用的对象是指被GC ROOTS直接引用的对象)。因此,灰色对象是垃圾,可以由垃圾收集器清理。
这种引用计数法的方式标记垃圾看着貌似很美好,实际上存在一个巨大的缺陷,就是循环引用。如下图中红色的对象形成的循环引用,每个对象的引用计数都不为零:
红色的对象实际上是应用不使用的垃圾。但由于引用计数法中存在循环引用的缺陷,仍然存在内存泄漏。有一些方法可以克服这个问题,例如使用特殊的“弱”引用或者应用独立的算法来收集循环。前面提到的语言--Perl、Python和PHP--都以这样或那样的方式处理循环,此处不做展开,我们将重点详细地研究JVM所采用的方法。
标记清除法(Mark and Sweep)
首先,JVM对构成对象可达性的因素更加具体。与前面看到的模糊定义的绿色的云不同,我们有一组非常具体和明确的对象,称之为垃圾收集根(Garbage Collection Roots)
- 本地变量(Local variables)
- 活动的线程(Active threads)
- 静态字段(Static fields)
- JNI引用(JNI references,Java Native Interface references)
JVM用来跟踪所有可达(活)对象并且确保不可达对象所占有的内存可以被重用的方法叫做标记清除算法,它包含两个步骤:
- 标记:从GC根开始遍历所有可达的对象,并在本机内存中保存这些对象的账本。
- 清除:确保不可达对象占用的内存地址可以被下一次分配重用。
JVM中不同的垃圾收集算法,如 Parallel Scavenge, Parallel Mark+Copy or CMS, 在实现这些阶段时略有不同,但在概念层面上,该过程与上面描述的两个步骤类似。
标记清除算法解决了内存泄漏:
不太好的是,在收集时需要停止应用线程,因为如果引用一直在变化的话,就无法真正的计算它们的数量。当引用程序被临时停止,以便JVM可以沉浸在内部活动中时,这种情况被称为Stop The World(Stop The World pause)。引发这种情况的原因可能有很多,但垃圾收集是迄今为止最流行的一种原因。
原文地址:https://plumbr.io/handbook/what-is-garbage-collection