现代的虚拟运行时平台基本上都提供了垃圾回收的机制,其实垃圾回收是一个非常古老的技术,可以追溯到Lisp。
那么为什么要使用垃圾回收呢?这个要和显式的内存分配和释放相比较。我们或多或少的接触过或学习过这样的代码:
1: struct Node{
2: ElementType Element;
3: Node* Next;
4: }
5:
6: Node* node = malloc(sizeof(Node));
7:
8: //do something
9:
10: free(node);
这里的malloc和free就是程序员显式的从堆上分配内存和释放不再使用的内存,将其归还给堆。这种让程序员控制内存的分配和释放有很大的灵活性,如果操作得当也非常高效。对于我上面写的这段代码我们都能很正确的写出分配和释放的代码。但是当项目越来越大,这种靠程序员自己分配和释放的机制就会带来一些问题,最典型的有下面两个:
1 我malloc了,但是忘记调用free。
这就是著名的内存泄露(memory leak)。表现的形式就是程序运行一段时间后所占的内存居高不下,只升不降。因为我们总是一味的去分配,而没有释放,甚至会出现out of memory错误。特别是对于那些长时间运行的应用(比如服务器端程序),内存泄露是致命的。可能有人要问,是你malloc的,你怎么就忘记free了呢?如果代码就寥寥数行,确实不容易出现这种问题,但是试想这样的情况:你在一个函数里使用malloc分配了一段内存,然后将指针作为返回参数返回给函数调用者,这个时候就很有可能忘记free了。
2 我已经free了,但是疏忽了,我还是使用了
这就是所谓的悬挂指针(hang pointer)。表现形式就是程序运行不稳定。有可能还得出错误的结果。
实际上最大的问题是,这两种错误都很难诊断。对于一般的bug,我们可以分析源代码,调试等手段很容易的得出。而这两种类型的bug,不仅需要借助一些专用的工具,还必须有耐心,就像守株待兔一样耐心的等待bug的出现。可想而知,那样的效率是如何低下。
为了将程序开发人员从这样的噩梦中拯救出来,现代的运行时都提供了各种各样的自动垃圾回收机制。
在具备自动垃圾回收机制的平台里,应用开发人员只管创建对象,垃圾回收器会帮你照料这些对象。垃圾回收器通过巧妙设计的算法发现那些不再被使用的对象(垃圾),然后将它们回收。
那么,既然垃圾回收机制如此美妙,而自己管理内存又有这么多问题,那是不是所有的编程平台都应该提供这种机制呢。
计算机里充斥着大量类似的权衡问题。很多机制都是一把双刃剑,我们需要知道它所适用的场景,而不应该发现它的好处就把它当做神灵。
由于内存资源并不是无限的,在程序运行过程中肯定会触发多次的垃圾回收。垃圾回收与应用程序一起(有可能是同时并行的运行)运行,肯定也要占用一定的系统资源。而且垃圾收集器线程工作时,基于某些原因还要停止当前应用程序中所有线程的执行(运行托管代码的线程),让垃圾收集器的线程干完事儿后再继续执行,这对于一些对实时性很高的应用来说是不能忍受的。
垃圾回收器所采用的策略也多种多样,比如对于.NET的有Workstation的,有Server的。对于Workstation又有Without concurrent和Concurrent的(在.NET 4.0中,已经被Backgourd所替代)。每种策略都有自己的特点,对某一方面有利,对另一方面则弱一些,所以要根据自己应用的特点选取合适的垃圾回收策略(这方面Java的可控性更高一些,可以调整很多参数进行优化)。