说到Java的GC大家应该都很熟悉,但随着JVM种类的不断增加,以及日益成熟的项目实践,使得GC(垃圾回收)技术经过了一代又一代的变迁。下面,我想要为大家描述一种现在使用最多的JVM GC算法----代分算法---的七大姑八大姨夫们。
在正式开始之前,想和大家扯扯GC的历史问题,这样可以使大家有一个稍微完整一些的概念。
时间轴 :
在Java出现之前,存在着一种伟大的语言,它的名字叫做C。C是Basic的亲儿子,它的爸爸教会了它各种技能。由于它太过优秀,任何事情都要亲历亲为才会使它感到放心。举个例子吧,C认为内存实在太重要了,所以一定要程序员自己管理。比如我要创建一个变量,需要先为一个结构体在堆中开辟一片内存空间(malloc)然后将引用这片内存空间的地址传递给变量。一旦这个变量没有用处了,必须销毁并清理这片内存空间(free)。当然,你可以选择不去销毁它。这并没有什么大不了的,顶多运行一段时间就会内存溢出报出和蔼可亲的“段错误”罢了。这时你有两个选择,一、重新开启这个程序。二、老老实实的为每个变量做销毁清理动作。
也许你觉得这没什么,只是多加一条语句罢了。好吧,也许你只是在写一个helloworld罢了。 当你真正开始用它来做事情的时候,数不清的变量等待着你去为它开辟/销毁,并且有些变量相互引用,我想这并不是一件简单的工作。
这时新生的Java出现了。与C相对应的,Java认为内存管实在太重要了,所以一定要自己管理。所有C程序员激动的内牛满面。。。 这就是GC对世界带来的变革。
使用了Java程序员腰不酸了,腿不疼了,一口气写一片,一行顶过去5行。。。
GC对Java带来的福利 :
说起GC所带来的好处,大家可能这样觉得,不就是可以自动清理没用的对象(内存空间)么。如果你真这样觉得,我想我应该很负责任的告诉你,孩子。。其实你什么都不懂。。
好吧,不绕弯子了。直接告诉你,那就是对内存创建和清除的速度优化!不可思议吧,一个清理内存系统居然会和创建内存空间的速度有关系。事实上确实如此,由于JVM的清理机制,使得大部分JVM上堆的实现和以前完全不同了,这里我们做一个对比。
*在之前的各种语言中:
如果你想要创建或者说开辟一片内存空间,要分三步,第一步把冰箱门打开。呃。。。第一步计算变量(对象/结构)的数据大小。第二步在堆中迭代查找足够大的一片内存空间。第三步在这片内存空间中创建变量(对象/结构)并把它的地址(引用/句柄。。反正就是这种东西了。。你懂的)传递给处于栈内的声明式变量(或者叫引用)。
*在Java中 :
现在你要创建一片开辟一片内存空间,只需要把堆的指针向下移动一格即可,就像一个链表一样。这与之前的“大杂院”堆开辟一片内存空间的速度完全不在一个次元!而着种改变实际上都应该归功于GC。是它促使了这种改变,使得Java在堆中创建对象可以和之前的一些语言在栈中创建的速度相媲美。一方面Java的堆的指针向下移动分配空间,另一方面GC清理内存空间并使它变的紧凑。这样,我们便拥有了近乎无限的、快速的可供分配堆模型。
Sun早期版本中的GC实现 :
在JVM早期,GC的实现还没有现在的分代那么完整,或者说是阉割版分代算法。它分为几个算法模式 :
* 引用计数器算法
引用计数器算法通常用来描述垃圾回收的工作机制,它是一个简单但代价高昂并有缺陷的算法。虽然经常用它来解释垃圾回收是如何工作的,但是至今未被任何JVM实现采用。但是,如果你有兴趣了解垃圾回收算法的编年史还是需要知道它的。
引用计数器是作用在堆中的对象的,一旦一个对象被引用一次,计数器的数值就会+1,反之则会-1。一旦引用计数器数值为0,就表示这个对象已经无用了,自然会被回收。但是它的缺陷是对象中循环引用的关系,试想一下,如果对象和对象之间互相嵌套,并相互引用,可能会导致一个对象应该被回收,但是它的引用计数器不为0。这就是导致至今它都未被应用于任何JVM中的一个致命元素。
*自适应垃圾回收算法
自适应式垃圾回收算法起始是引用计数器的逆向思维,它不去记录对象到底被多少对象所引用,而是监测全部局部变量,静态变量等有没有引用一个对象,如果没有,则那些对象必定是没有用的,这就解决了对象之间循环引用的问题。这么做的思想是“如果一个对象的生命周期没有结束,则它一定会追朔到【活在】栈或静态区域的引用”。那么自适应在哪里呢 ? 在于它还有着两种模式:停止--复制、标记--清扫。
停止--复制: 这种模式会先暂停程序将存活的对象从一个堆复制到令一个堆(你可以想想虚拟机将堆分成了很多块,事实上也是如此,堆中被分配了很多区域)没有被复制的全部都是垃圾,在这之后将被全部销毁。复制到令一个堆之后需要修正引用地址,它的好处是,清理之后堆中对象时一个挨着一个的。(上述的堆实现提到过很多JVM种堆的实现就想一个链表一样)但是缺点也很明显,大量的复制,会有严重的效率问题。所以会有另一种模式。
标记--清扫: 这种模式同样会先暂停程序,并循环遍历栈和静态区内的所有对象的引用,一旦找到一个存活的对象便会给这个对象加上一个标记,结束后,清理掉所有没有被标记的对象。这种方式是最常被使用的清理方式,因为一旦程序稳定运行,只会产生少量垃圾或者没有垃圾。一旦你了解这种方式基本不会产生什么垃圾,就会体会到这种方式是很迅速的。但是速度的代价就是这种方式清理过后堆中的对象并不是连续的,如果想要连续则需要对剩下的对象进行一个整理的动作。
说到停止--复制,在大多数JVM实现中,并不会复制全部对象,因为这些JVM的堆实现中,堆分为很多块,很大的对象会独占一个块,其他较小的对象会被复制到已经废弃的块中去(你可以想象有很多个花盆,花花草草的一起种在一个盆里,像木头那种就要自己一个盆了)。这样,就可以减少不必要的浪费(当然每个块中都有一个记录块中对象是否被引用的计数)。这样对于清理很多短命的局部对象时很有用的。
说到自适应,是因为这两种模式会相互切换。你可以想象一下,在堆中垃圾和碎片并不多的情况下,肯定是会使用标记-清扫这种方式。一旦监测出堆中出现很多碎片或垃圾的时候,就会切换到停止-复制。这是很有用的,急于这两种模式的切换,保证了一个快速的、近乎无限的堆的实现,并保证了垃圾回收的质量。
以上就是sun早期的一些GC实现,尽管不是很详细,但我想大家已经清楚设计者的思路了吧。在现在,已经有了很多替代者实现了更高效、更人性化的GC实现。下一篇将写一个现代JVM中最常被使用的算法----分代回收算法。