什么是垃圾回收?
从字面看来,按字面意思来理解就是--找到垃圾对象并将他们抛弃掉;事实却正好相反,垃圾回收是把处于活动状态的对象找出来,而将剩余的对象标记为垃圾对象。基于此理论,我们来详细描述java virtual machine的自动垃圾回收机制。
我们首先从最基础的垃圾回收的一些属性、概念、方法讲起,而不会直奔主题:JVM的垃圾回收机制。
声明: 这边文章主要论述Oracle HotSpot和OpenJDK的垃圾回收机制。至于其它的一些JVM,例如JRockit或者IBM J9,它们可能采用了不同机制来实现垃圾回收。
手动管理内存(Mannual memory management)
在介绍现在流行的自动垃圾回收机制之前,先让我们回到手动为数据分配内存,然后再手动回收这些内存的时代吧。在那个年代,如果你忘记了释放这些已分配的内存,那么就不能再重新使用这些内存空间了。内存虽然已经被声明进行了分配,但是确不能再使用,我们管这样的场景就叫:内存泄漏。
下面是一段用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;
}
从代码当中我们可以看出,很容易忘记去释放内存,从而发生内存泄漏问题。发生内存泄漏以后,我们只能通过排查代码的方式找到具体的原因。所以,最好的方式就是有一种机制,自动的去回收不再使用的内存,从而降低人为错误的概率。这种自动回收内存的机制就叫Garbage Collection(简称:GC)。
智能指针
实现自动化垃圾回收的最直接的一种方式就是利用destructor。例如,我们可以利用C++里得vector来实现,当变量从vector脱离以后,destructor就会自动被调用,从而回收内存:
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;
}
但是在一些复杂的场景中,特别是共享对象被多线程同时引用的时候,单单利用destructor明显不能满足需求。基于以上的场景,最简单的方式就是:对对象的被引用进行计数。针对每个对象,我们会记录下这个对象当下被引用的次数是多少,当被引用的次数变为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;
}
为了避免函数在被调用的时候,元素再一次被读取,我们可以将其放进缓存,在这种场景下,利用vector来进行对象销毁就不可行了。因此,我们改为利用shared_ptr,它将持续关注元素的被引用数,当指针在其它地方被引用的时候,引用数就会加一,当指针被释放的时候,引用数就会减一,当引用数被减为0时,shared_ptr就是删除所关联的vector。
自动化内存管理(Automated memory management)
观察上面的C++代码,我们可以明确什么时候需要关心内存的管理。但是,我们如果将这种机制应用到所有的对象上面将会怎么样?这将使得我们的工作变得非常顺手,因为,作为开发者,再也不用关心垃内存回收的问题了。在运行的过程当中,将自动检测出不再被使用的对象,并清除它们以便释放内存空间。换句话说,就是自动进行垃圾回收。第一个垃圾回收器是LISP语言的,诞生于1959年,以后的日子了,这项技术在不断的进步。
引用计数法(reference counting)
很多的语言,例如Perl、PHP、Python都是采用我们上文提到的C++共享指针的方式进行垃圾回收。通过下图可能展示的更清晰:
绿色的云区域表示被他们所引用到的对象还在被程序所使用,这些对象可能是当前运行方法里的本地变量,也可能为一些静态变量,或者其它的一些对象。每种语言与每种语言在这方面可能会有不同,但这些不是重点。
蓝色的链路代表内存中的活跃对象调用链路,圆圈的数字标识了这个对象当前被引用的次数。灰色的链条代表了那些没有再被显示引用到的对象(指得是那些被绿色的云引用到的对象)。灰色的对象就是可以被垃圾回收器回收的垃圾对象了。
这个方法看起来真的很好,但是却有一个致命的缺陷,就是会产生分离的垃圾闭环,上面的对象实际上已经没有意义了,但是它们的被引用数又都不为零。如下图所示:
图中的红色闭环,实际上已经成为了应用程序不再使用的垃圾对象,但是由于被引用数不为零,所以不能被回收,从而造成了内存泄漏的问题。
有很多的办法可以解决这个问题,例如可以利用弱引用的方式(‘weak’reference),也可以采用单独的算法对闭环链路进行垃圾回收。 我们上文提到的Perl、Python、PHP都会采用其中的一种方式来进行垃圾回收。 我们将不会对它们的实现进行详细描述,而是在后续的章节当中,对JVM的机制进行详细解读。
标记清除(Mark and Sweep)
在如何遍历活跃对象的方法上JVM比较特别,不像上节似的定义的绿色的云是模糊不清的,JVM明确的定义了几组叫做Garbage Collection Roots的对象:
- Local variables
- Active threads
- Static fields
- JNI references
我们将JVM对所有活跃对象进行追踪标记,而将未标记的对象进行内存回收再分派的过程叫做Mark and Sweep算法,主要由两步来完成。
Mark阶段,从GC roots对象开始对可到达的对象进行一次遍历标记,并对所有这些对象,在本地内存中做一个分类。
Sweep阶段,将那些没有被遍历到的对象占用的内存空间进行回收,以便在下次的内存分派过程重新使用。
JMV有很多种的垃圾回收算法,比如说Parallel Scavenge,Parallel Mark+Copy 或者CMS,他们基本上都是按照上述方式实现的,细节可能会有不同,但是核心思想都是按照上述的两步来进行实现的。
这种方式一个重要的优点就是避免了因垃圾闭环而导致的内存泄漏问题:
而缺点就是,当进行垃圾回收的时候,所有的应用线程都要进行一下暂停。因为如果程序不暂停,就没有办法准确的计算出每个对象的被引用数是多少。我们把这种为了能够准确的找出活跃对象,而让程序临时暂停的方式叫做:Stop the word pause,这种暂停的发生可能有很多的原因,但其中最重要的就是为了进行垃圾回收。