龙珠

修炼自己与发现世界

Java的垃圾回收机制

在C++中,对每个对象生成时的内存分配,到结束时的内存回收,都是需要程序员来处理的。如果忘记了回收内存,对象所占用内存长期存在,无法回收,就是内存泄露。

内存回收是个很麻烦的问题, 需要程序员很小心的处理,消耗大量精力。对于这同一个问题,C++和Java的处理上截然不同。C++设计者认为,内存的处理如此麻烦,因此一定要程序员自己处理;而Java的设计者认为,内存的处理如此麻烦,因此一定不能让程序员自己处理(所以Java中自带垃圾回收机制)。从我个人的感觉而言,Java的垃圾回收机制让程序员摆脱伤脑筋的内存管理,从而更容易上手。但是深入来看,C++的内存管理更贴近机器处理的内部,所以更能写出高效的代码,相对而言,Java代码的效率要低一些。

在Java进行垃圾回收之前,是需要调用对象的finalize方法进行清除。不过,这并不是个推荐使用的方法。具体可见我的另一篇文章:Java:finalize方法介绍和为什么不要使用它?【存疑】

下面回到我们的话题:Java中垃圾回收器(GC)是如何工作的?

在JVM中有一个GC(Garbage Collection)线程,它在工作时,将一面回收空间,一面使堆中的对象紧凑排列。这样,实现了一种高速的、有无限空间可供分配的堆模型。

我们先来讲一个简单但速度很慢的垃圾回收技术(这种技术常被用来讲解垃圾收集的工作方式,但很少用于实际JVM中):计数法。

对于每个对象,都有一个引用计数器。当有新引用时,计数器+1; 当引用离开作用域或被置为null时,引用计数-1。而垃圾回收器会在包含全部对象的列表上遍历,当发现某个对象的引用计数为0时,就释放该空间。不过,这个方法有个缺陷:如果对象之前存在循环引用,就会出现“对象应该被回收,但是引用计数不为0”的情况。

在更快的模式中,找到需要回收/存活的对象是另一种技术。这种技术的基本思想是:对于任何“活“的对象,一定能追溯到其存活在堆栈或静态存储区之中的引用。这个引用链条可能穿过好几个对象层次。因此,如果从堆栈和静态存储区开始,遍历所有引用,就能找到所有“活”的对象。这样就解决了“交互引用的对象组”问题——这些对象根本不会被探测到,因此其内存空间被JVM自动回收。

在找到需要垃圾回收/存活的对象之后,有的JVM中使用停止-复制(stop-and-copy)方法。先暂停程序的运行,之后将所有存活的对象从当前堆复制到另一个堆,没有复制的全部是垃圾进行回收。这样做有个缺点:首先,这需要两个堆,GC在这两个堆之间倒腾,需要维护多一倍的空间。而且,对象移动后,所有指向他的引用都必须修正。其次,复制很耗费时间。尤其是系统进入稳定态后,只产生少量垃圾,那么大量复制就不是必要的,反而很浪费时间。另外,这个方法中,垃圾回收动作不是在后台进行,在垃圾回收时程序会被暂停。

这样,有些JVM会进行检查,如果没有新垃圾产生,就自动转入另一种工作模式,一般称为标记-清扫(mark-and-sweep)。这种方法的思路是:在遍历堆栈和静态存储区时,遍历所有引用,找到“存活”的对象。但是,这个过程并进行垃圾回收,而是对每个“存活”的对象进行标记。只有全部标记工作完成之后,清理工作才会开始。在清理过程中,没有标记的对象将被释放,不发生任何复制动作。所以剩下的堆空间是不连续的。GC过希望得到连续的空间的话,就需要重新整理剩下的对象。这种模式一般很慢,但是如果新产生垃圾很少的时候,就很快了。

我们来整理一下思路:

垃圾标记技术有两种:

1. 计数法。缺点:会交互引用的问题。

2. 从堆栈和静态存储空间遍历引用法。

第一种标记技术基本不使用了,在使用第二种方法标记之后,垃圾回收也有两种模式:

2.1 停止-复制(stop-and-copy)方法。缺点:1. 需要多一倍空间;2. 复制耗费时间;3. 程序会被暂停。

2.2 标记-清扫(mark-and-sweep),即“自适应”方法。缺点:速度很慢,容易在堆空间产生很多碎片。适合新产生垃圾较少的情况。

实际JVM中,垃圾回收器会定期进行完整的清理动作。

1. 先使用停止-复制方法:大型对象不会被复制,包含小对象的块则被复制并整理。每个块都有相应的代数(generation count)来记录它是否还存活。

2. JVM会进行监视,如果所有对象很稳定,垃圾回收器效率降低的话,就切换到标记-清扫方式。

3. 同样,JVM继续监视效果,如果堆空间出现很多碎片,则继续切换回“停止-复制”方式。

这就是GC的“自适应”技术。也可以称为“自适应的,分代的,停止-复制,标记-清扫”式垃圾回收器。

不过,Java中并没有强制执行垃圾清理的方法。System.gc()只能是建议JVM进行垃圾清理,JVM并不保证在System.gc()之后一定执行垃圾清理。

JVM中,GC的执行的目的是回收程序不再使用的内存,一般会在后台执行,无法由用户直接控制(这是我觉得不爽的一点,因为会有程序的执行不在你的掌控之中)。

 

参考资料:

  1. 《Java编程思想》

  2. Object.java