我发现很多人都不懂的怎么管理内存,一旦失去了 GC,就不知道怎么释放内存了。本人写了几十年程序,自始自终没有感觉到这是一个问题。下面这些小经验可以让你让你在绝大多数情况下都游刃有余。

成对的分配和释放

当你写分配内存的代码的时候,同时也写内存释放的代码。然后再写别的代码。这样的好处是可以避免忘了释放内存。

“谁分配?谁释放?”原则

任何一个程序,首先要回答一个问题:“谁分配?谁释放?”,并将这作为一个 原则,贯穿整个程序的始终。掌握这个原则,基本上内存管理是不会乱的。

一个程序,对这个问题的回答,最好只有一种。在这方面,Delphi 及其自带的库给了非常好的示范。Delphi 本身是没有 GC 的语言,也没有 RAII,但是你从来不会听 Delphi 程序员跟你说“内存分配和释放搞不懂,太难了”。为什么呢?Delphi 的内存分配和释放只有一种方法,和 C 一样,这简化了内存管理,整个程序里只有各种各样的指针,没有 C++ 一样的的 RAII 机制。另一方面,也是最关键的,Delphi 及其自带的库(VCL, FireMonkey 等为主)都会和程序员们进行一个约定:“谁分配?谁释放?”,并且这个原则贯穿始终,所以只要程序员也同时遵循这个约定,再复杂的程序也不会乱,内存管理变成一件简单的事情。

同样的,一个编程语言,要么提供GC,要么只提供一种内存分配/释放的方法,是最佳的。比方说C就只使用 malloc()/free(),而 C++ 提供了 RAII,使得程序员、库会主动的使用 RAII (添乱),于是内存分配和释放变得混乱而复杂。

而只提供一种内存分配/释放的方法的编程语言,往往也会把内存管理的原则“谁分配?谁释放?”推广形成一种文化,使得不仅核心库,各种周边的库和程序也都会首先遵循这种原则。Delphi 仍旧是这种现象的很好典范。

时刻清楚自己在做什么

Delphi 其实也有一种引用计数的机制[1],最早是为了对接 COM 而引入的,因为 COM 本身就使用这种机制。到 Delphi 7 的时候,这已经是一种语言的核心机制。然而,到目前为止,尽管引用计数可以自己释放内存,但除了对接 COM 以外极少被使用。

为什么呢?一方面,这种不同的机制会打乱基本的内存分配释放机制,让内存管理变得复杂。另一方面,Delphi 程序员从入门开始,受到的教育是:“时刻确保正确的引用数”。这是一种“时刻清楚自己在做什么”的教育。Delphi 的教程会告诉你,由于各种各样的原因,引用计数总会有因为意外导致计数错误的情况,这种情况要自己手动的修正引用计数。

复杂引用的释放

对于引用非常错综复杂的情况,通常这时候对象之间的引用已经形成了一张像蜘蛛网一样的图,很多人往往不知道怎么下手。这时候最简单的方法,就是按照分配的相反顺序,一个个的进行释放。对于无法知道分配顺序的,则可以按照下面的方法来。

首先,要明白,任何复杂的关系都有一个主轴。比如 GUI 界面,里面各种控件互相引用错综复杂,但是“谁拥有谁”这种关系就是一个主轴,其他的引用关系可以忽略(就像 weak reference)。

寻找这个主轴非常重要。一旦找到这个主轴,引用的主次就很明显了,照着这个主轴来释放就行了。其实这和 GC 很像,但是 GC 的用户通常来说极少用 weak reference ,使得看起来好像每个引用都很重要。

有时候,主要的引用(主轴)之间,会形成一个循环链表或类似的回路。通常来说,我们需要释放这种结构的内存,说明这整一个回路的内存都是需要释放的。这时候,可以从其中一个对象开始释放,强制切断主轴之间的引用关系,一个个的进行释放。

GC

写到这里,感觉手动释放内存和 GC 是很接近的。但是和 GC 相比,手动释放是更高级的更精确的,因为我们知道内存中的数据之间的逻辑关系,这是垃圾回收器无法知晓的。

其实像 C/C++ 一类的不带 GC 的语言,也是可以使用 GC 的。只是知道的人并不多。比方说,著名的 boehm gc ,就是一个事实标准的 C/C++ 的 GC 库,很多带 GC 的编程语言的 GC 也是用它实现的。C 语言使用这个库是十分简单的,强烈推荐~


  1. Delphi 中传统长字符串也是引用计数的机制,但不是这里说的机制。这里说的是 interface↩︎