如何优雅的释放内存(二):使用 GC 库
在 上一篇文章 中,我们提到 C 语言中可以使用 GC 库。这个 GC 库就是 The Boehm-Demers-Weiser conservative C/C++ Garbage Collector ,常说的 bdwgc, libgc, boehm-gc 都是指这个 GC 库。基本上,一个带 GC 的编程语言的实现,如果不是选择自己实现 gc 库(如官方 JVM),都会使用 boehm-gc 。下面介绍这个库的使用。
观念
在使用 boehm-gc 之前,先要回答“谁分配?谁释放?”的问题,并作为原则贯彻始终。一个简单的策略可以是:
-
内存分成两部分: GC 内存 和 foreign 内存 。
- foreign 内存指引用的第三方库的内存。这里“第三方库”包括标准库。
- GC 内存指当前程序的内存(非第三方库部分)。除了 foreign 内存其它部分都使用 GC 管理。
-
foreign 内存的的指针,和 gc 内存的指针,不混用,程序中应该能明显看出来。
- 通常我们不把 GC 内存的指针给第三方库。如果确实有这种需求,应该增加相应的引用,并维护引用数。
- 很多 foreign 对象的使用,可能需要通过被 GC 对象引用来进行。对于 foreign 对象的 GC wrap (或引用),必须添加 finalizer,在 GC 的时候释放 foreign 内存。
- 为了避免混乱,确保一个 foreign 对象只被一个 GC 对象引用。
使用 boehm-gc
在 Ubuntu 22.04 中使用,可以安装软件包 libgc-dev
。如果是全平台程序,那么可以通过 vcpkg 引入 bdwgc
包来使用。虽然知道这个库的人并不多,但其实这个库使用范围相当广泛,基本上是这个领域的一个事实标准,所以各种系统中都能找到这个库。
boehm-gc 的使用十分简单,只需 #include <gc.h>
,然后使用 GC_malloc(size)
分配内存即可。GC_malloc()
函数的使用和标准库里的 malloc()
用法是一样的,差别只在于,它分配的内存是受 GC 管理的。下面是一个简单的使用例子。
1 |
|
编译和执行
1 | $ gcc gc1.c -lgc -ldl && ./a.out |
这个例子演示了 boehm-gc 的基本使用方法。对于绝大多数情况来说,这里介绍的已经十分够用了。除了前边介绍的 GC_malloc()
,GC_register_finalizer(obj, proc, data, 0, 0)
用于注册对象对应的 finalizer 。在对象被回收之前,这个 finalizer 会被调用。
程序最后补上 GC_gcollect();
,是为了演示对 finalizer 的调用。有一点要注意的是,默认情况下,程序退出时是不会 GC 的[1]。这是没有必要的,程序退出的时候,各种资源自然会被系统释放。另一方面, boehm-gc 使用这个机制来做内存泄漏检测,在程序退出的时候,可以知道还有哪些内存没有释放。作为内存泄漏检测器,是 boehm-gc 的另一个用法。例如上面的程序,注释掉最后一句,在程序开头启用内存泄漏检测,如下所示。
1 |
|
编译和执行
1 | $ gcc gc1.c -lgc -ldl && ./a.out |
这样就列出了程序退出时未释放(泄漏)的内存。
结论
boehm-gc 的确十分方便了 C 语言的使用,直接让 C 语言变成了 golang ,但也因为“方便”而增加了内存管理的难度(多了一套内存管理机制)。明确区分 GC 内存和非 GC 内存,并确保相应的使用原则,保证不混乱,是十分重要的。
boehm-gc 其实也可以被 C++ 使用,但由于 C++ 的内存分配相关的语言机制十分复杂且可被高度定制,极大的增加了 C++ 的使用难度,所以通常不在 C++ 上使用。如果一个 C++ 库被 wrap 给一个带 GC 的程序使用,通常做法是先 wrap 成不带 GC 的 C 语言库,然后再进行带 GC 的 wrap,以减少内存管理的难度。类似的例子数不胜数,例如 Qt,就是先得到 C 语言的 wrap,再把 C 语言的 wrap 包装给 Python 等带 GC 的语言使用。
boehm-gc 会自己选择恰当的时机来释放内存,但不是程序退出时。 ↩︎