上一篇文章 中,我们提到 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <gc.h>
#include <stdio.h>

struct S {
int id;
};

void S_finalizer(void * object, void * data) {
struct S * p = (struct S *)object;
printf("Called finalizer for object at %p with id = %d\n", p, p->id);
}

int main()
{
for (int i=0; i<5; i++) {
struct S * p = (struct S *)GC_malloc(sizeof(struct S)); // allocates memory using boehm-gc
p->id = i;
printf("Allocates object at %p with id = %d\n", p, p->id);
GC_register_finalizer(p, S_finalizer/* proc */, 0/* data */, 0, 0); /* register finalizer for allocated object */
}

/* perform GC before program exit */
GC_gcollect();
}

编译和执行

1
2
3
4
5
6
7
8
9
10
$ gcc gc1.c -lgc -ldl && ./a.out 
Allocates object at 0x7f2bcb9aaff0 with id = 0
Allocates object at 0x7f2bcb9aafd0 with id = 1
Allocates object at 0x7f2bcb9aafc0 with id = 2
Allocates object at 0x7f2bcb9aafb0 with id = 3
Allocates object at 0x7f2bcb9aafa0 with id = 4
Called finalizer for object at 0x7f2bcb9aafb0 with id = 3
Called finalizer for object at 0x7f2bcb9aafc0 with id = 2
Called finalizer for object at 0x7f2bcb9aaff0 with id = 0
Called finalizer for object at 0x7f2bcb9aafd0 with id = 1

这个例子演示了 boehm-gc 的基本使用方法。对于绝大多数情况来说,这里介绍的已经十分够用了。除了前边介绍的 GC_malloc()GC_register_finalizer(obj, proc, data, 0, 0) 用于注册对象对应的 finalizer 。在对象被回收之前,这个 finalizer 会被调用。

程序最后补上 GC_gcollect(); ,是为了演示对 finalizer 的调用。有一点要注意的是,默认情况下,程序退出时是不会 GC 的[1]。这是没有必要的,程序退出的时候,各种资源自然会被系统释放。另一方面, boehm-gc 使用这个机制来做内存泄漏检测,在程序退出的时候,可以知道还有哪些内存没有释放。作为内存泄漏检测器,是 boehm-gc 的另一个用法。例如上面的程序,注释掉最后一句,在程序开头启用内存泄漏检测,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <gc.h>
#include <stdio.h>

struct S {
int id;
};

void S_finalizer(void * object, void * data) {
struct S * p = (struct S *)object;
printf("Called finalizer for object at %p with id = %d\n", p, p->id);
}

int main()
{
GC_set_find_leak(1); // enable memory leaks detection

for (int i=0; i<5; i++) {
struct S * p = (struct S *)GC_malloc(sizeof(struct S)); // allocates memory using boehm-gc
p->id = i;
printf("Allocates object at %p with id = %d\n", p, p->id);
GC_register_finalizer(p, S_finalizer/* proc */, 0/* data */, 0, 0); /* register finalizer for allocated object */
}

/* perform GC before program exit */
// GC_gcollect();
}

编译和执行

1
2
3
4
5
6
7
8
9
10
11
12
$ gcc  gc1.c -lgc -ldl && ./a.out 
Allocates object at 0x7f9ac60e7ff0 with id = 0
Allocates object at 0x7f9ac60e7fe0 with id = 1
Allocates object at 0x7f9ac60e7fd0 with id = 2
Allocates object at 0x7f9ac60e7fc0 with id = 3
Allocates object at 0x7f9ac60e7fb0 with id = 4
Found 5 leaked objects:
object at 0x7f9ac60e7fb0 of appr. 16 bytes (composite)
object at 0x7f9ac60e7fc0 of appr. 16 bytes (composite)
object at 0x7f9ac60e7fd0 of appr. 16 bytes (composite)
object at 0x7f9ac60e7fe0 of appr. 16 bytes (composite)
object at 0x7f9ac60e7ff0 of appr. 16 bytes (composite)

这样就列出了程序退出时未释放(泄漏)的内存。

结论

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 的语言使用。


  1. boehm-gc 会自己选择恰当的时机来释放内存,但不是程序退出时。 ↩︎