一、介绍
在 C 语言的世界里,内存管理始终是悬在开发者头顶的一把剑。每一次 malloc 都意味着必须有对应的 free,一旦遗漏,内存泄漏便悄然滋生。tgc(Tiny Garbage Collector)是由 Daniel Holden(orangeduck)开发的一个轻量级 C 语言垃圾回收库,仅用约500 行代码实现了一个保守式、标记-清除(mark and sweep)垃圾回收器,让 C 程序员也能享受自动内存管理的便利。
![]()
项目地址:https://github.com/orangeduck/tgc Star 数:1.1k+ 核心文件:tgc.h + tgc.c,开箱即用二、特性
- 极度轻量:整个实现约 500 行 C 代码,头文件 + 源文件各一个,零依赖
- 保守式 GC:无需修改数据结构,兼容现有 C 代码
- 标记-清除算法:经典可靠,内存回收安全有效
- 支持析构器:可为每个 GC 管理的对象注册析构函数,自动释放系统资源
- 支持根对象(ROOT):手动控制不参与自动回收的长生命周期对象
- 支持叶对象(LEAF):标记不含指针的内存块,跳过扫描,提升性能
- 线程本地:每个线程独立维护 GC 实例,避免多线程竞争
- 跨平台:已在 Linux、Windows、macOS 上验证,成功集成到 bzip2、oggenc 等实际项目
tgc 的架构极为简洁,核心思路是:
- 维护所有分配列表:通过 tgc_alloc 系列函数分配内存时,将指针、大小、标志、析构器等元信息记录到内部表中
- 扫描可达内存:在 GC 触发时,扫描栈内存和 GC 堆内存,找出所有可达指针
- 清除不可达内存:对比两个列表,释放不可达的内存块
┌──────────────────────────────────────────────┐│ tgc_t ││ ┌─────────────┐ ┌──────────────────────┐ ││ │ stack_top │ │ items[] 分配记录表 │ ││ │ stack_btm │ │ ptr | size | flags │ ││ │ paused │ │ dtor | mark bit │ ││ └─────────────┘ └──────────────────────┘ │└──────────────────────────────────────────────┘│ │扫描栈内存 扫描堆内存└──────────┬─────────┘标记可达对象清除不可达对象(调用析构器→释放内存)3.2 可达性规则一个内存块被认为可达,当且仅当:
- 栈上(至少比 tgc_start 调用深一层的函数栈帧中)存在指向它起始地址的指针,或
- 另一个由 GC 管理的内存块中存在指向它起始地址的指针
不会被识别为可达的情况:
- 指针指向内存块内部(而非起始地址)
- 指针位于静态数据段(static 变量)
- 指针来自非 GC 分配的内存(如原生 malloc)
- 指针来自其他线程
- 调用 tgc_alloc 系列函数时,若当前分配数量超过阈值,自动触发一次标记-清除
- 调用 tgc_run 手动触发
- 调用 tgc_stop 时,释放所有剩余内存
将 tgc.h 和 tgc.c 复制到项目中,然后一起编译:
gcc your_program.c tgc.c -o your_program4.2 最简示例#include "tgc.h"#include#includestatic tgc_t gc;static void example_function() {// 分配内存,GC 会在不可达时自动释放char *message = tgc_alloc(&gc, 64);strcpy(message, "No More Memory Leaks!");printf("%s\n", message);// 无需手动 free!int main(int argc, char **argv) {// 启动 GC,传入当前栈帧地址作为扫描起点tgc_start(&gc, &argc);example_function();// 停止 GC,释放所有剩余托管内存tgc_stop(&gc);return 0;4.3 完整 API 速查函数
tgc_start(gc, stk)
启动 GC,stk 为栈顶地址
tgc_stop(gc)
停止 GC,释放所有内存
tgc_run(gc)
手动触发一次 GC
tgc_pause(gc)
暂停 GC 自动触发
tgc_resume(gc)
恢复 GC 自动触发
tgc_alloc(gc, size)
分配托管内存
tgc_calloc(gc, num, size)
分配并清零托管内存
tgc_realloc(gc, ptr, size)
重新分配托管内存
tgc_free(gc, ptr)
手动释放托管内存
tgc_alloc_opt(gc, size, flags, dtor)
带标志和析构器的分配
tgc_set_dtor(gc, ptr, dtor)
为已有内存注册析构器
tgc_set_flags(gc, ptr, flags)
设置内存标志
tgc_get_flags(gc, ptr)
获取内存标志
tgc_get_dtor(gc, ptr)
获取析构器
tgc_get_size(gc, ptr)
获取分配大小
五、各功能模块详解与代码示例5.1 基础内存分配
tgc_alloc 和 tgc_calloc 是最常用的分配接口,用法与 malloc/calloc 完全对应:
#include "tgc.h"#includestatic tgc_t gc;void basic_alloc_demo(void) {// 分配 100 字节,内容未初始化int *arr = tgc_alloc(&gc, sizeof(int) * 100);for (int i = 0; i < 100; i++) arr[i] = i;// 分配 10 个 int 并初始化为 0int *zeros = tgc_calloc(&gc, 10, sizeof(int));printf("zeros[0] = %d\n", zeros[0]); // 输出 0// 函数返回后,arr 和 zeros 变为不可达// GC 会在下次触发时自动释放int main(int argc, char **argv) {tgc_start(&gc, &argc);basic_alloc_demo();tgc_run(&gc); // 手动触发回收tgc_stop(&gc);return 0;5.2 析构器(Destructor)析构器允许你在 GC 释放内存前执行清理逻辑,非常适合管理文件句柄、网络连接等系统资源:
#include "tgc.h"#include#includestatic tgc_t gc;typedef struct {File *fp;char name[256];} ManagedFile;// 析构器:GC 释放 ManagedFile 前自动调用void file_destructor(void *ptr) {ManagedFile *mf = (ManagedFile *)ptr;if (mf->fp) {fclose(mf->fp);printf("自动关闭文件: %s\n", mf->name);void open_and_use_file(const char *path) {// 分配时直接指定析构器ManagedFile *mf = tgc_alloc_opt(&gc, sizeof(ManagedFile), 0, file_destructor);mf->fp = fopen(path, "r");snprintf(mf->name, sizeof(mf->name), "%s", path);if (mf->fp) {printf("文件已打开: %s\n", mf->name);// 使用文件...// 函数返回后 mf 不可达,GC 触发时自动调用 file_destructor 并 fclose// 也可以为已有内存事后注册析构器void register_dtor_later(void) {int *data = tgc_alloc(&gc, sizeof(int) * 50);tgc_set_dtor(&gc, data, [](void *p){ printf("data 被释放\n"); });int main(int argc, char **argv) {tgc_start(&gc, &argc);open_and_use_file("/tmp/test.txt");tgc_run(&gc); // 触发回收,自动关闭文件tgc_stop(&gc);return 0;5.3 根对象(TGC_ROOT)被标记为 TGC_ROOT 的内存不会被 GC 自动回收,必须手动调用 tgc_free 释放。适用于全局配置、静态数据段中引用的对象:
#include "tgc.h"#include#includestatic tgc_t gc;static char *global_config = NULL; // 静态数据段中的指针void init_config(void) {// 标记为 ROOT,GC 不会自动释放global_config = tgc_alloc_opt(&gc, 256, TGC_ROOT, NULL);strcpy(global_config, "app_mode=production;max_conn=100");printf("配置已加载: %s\n", global_config);void cleanup_config(void) {// 必须手动释放 ROOT 对象tgc_free(&gc, global_config);global_config = NULL;printf("配置已释放\n");int main(int argc, char **argv) {tgc_start(&gc, &argc);init_config();// 即使触发多次 GC,global_config 也不会被释放tgc_run(&gc);tgc_run(&gc);printf("GC 后配置仍在: %s\n", global_config);cleanup_config();tgc_stop(&gc);return 0;5.4 叶对象(TGC_LEAF)TGC_LEAF 告诉 GC 这块内存中不含指向其他 GC 对象的指针,GC 扫描时可以跳过它,显著提升大内存块(如字符串、图像数据)的性能:
#include "tgc.h"#include#includestatic tgc_t gc;void large_buffer_demo(void) {// 分配 1MB 的字符串缓冲区,标记为 LEAF// GC 不会扫描这块内存(因为里面不会有 GC 指针)char *buf = tgc_alloc_opt(&gc, 1024 * 1024, TGC_LEAF, NULL);memset(buf, 'A', 1024 * 1024 - 1);buf[1024 * 1024 - 1] = '\0';printf("缓冲区前10字节: %.10s\n", buf);// 函数返回后自动回收,且扫描时因 LEAF 标志而高效跳过int main(int argc, char **argv) {tgc_start(&gc, &argc);large_buffer_demo();tgc_stop(&gc);return 0;5.5 手动控制 GC在性能敏感的代码段(如实时渲染循环),可以暂停 GC,避免不可预测的停顿:
#include "tgc.h"#includestatic tgc_t gc;void realtime_loop(void) {// 暂停自动 GCtgc_pause(&gc);for (int frame = 0; frame < 60; frame++) {// 每帧分配的临时数据float *temp = tgc_alloc(&gc, sizeof(float) * 1000);// ... 渲染处理 ...(void)temp;printf("渲染第 %d 帧\n", frame + 1);// 恢复 GC 并手动触发一次tgc_resume(&gc);tgc_run(&gc);printf("GC 已完成清理\n");int main(int argc, char **argv) {tgc_start(&gc, &argc);realtime_loop();tgc_stop(&gc);return 0;5.6 realloc 动态扩容#include "tgc.h"#includestatic tgc_t gc;void dynamic_array_demo(void) {int capacity = 10;int *arr = tgc_calloc(&gc, capacity, sizeof(int));for (int i = 0; i < 50; i++) {if (i >= capacity) {capacity *= 2;// 动态扩容,GC 自动跟踪新地址arr = tgc_realloc(&gc, arr, capacity * sizeof(int));arr[i] = i * i;printf("arr[49] = %d\n", arr[49]); // 49*49 = 2401int main(int argc, char **argv) {tgc_start(&gc, &argc);dynamic_array_demo();tgc_stop(&gc);return 0;5.7 构建 GC 管理的链表#include "tgc.h"#includestatic tgc_t gc;typedef struct Node {int val;struct Node *next;} Node;Node *create_list(int n) {Node *head = NULL;for (int i = n - 1; i >= 0; i--) {Node *node = tgc_calloc(&gc, 1, sizeof(Node));node->val = i;node->next = head;head = node;return head;void print_list(Node *head) {while (head) {printf("%d -> ", head->val);head = head->next;printf("NULL\n");int main(int argc, char **argv) {tgc_start(&gc, &argc);Node *list = create_list(5);print_list(list); // 0 -> 1 -> 2 -> 3 -> 4 -> NULL// 丢弃 list 引用后,所有节点均不可达list = NULL;tgc_run(&gc); // 一次性回收所有节点printf("链表已被 GC 回收\n");tgc_stop(&gc);return 0;5.8 编译器优化下的正确用法当开启编译器优化(-O2)时,函数可能被内联,导致 GC 扫描不到变量。解决方法是使用 volatile 函数指针调用:
#include "tgc.h"static tgc_t gc;void my_program_logic(void) {char *buf = tgc_alloc(&gc, 128);// 使用 buf ...(void)buf;int main(int argc, char **argv) {tgc_start(&gc, &argc);// 通过 volatile 函数指针调用,防止编译器内联void (*volatile func)(void) = my_program_logic;func();tgc_stop(&gc);return 0;六、应用场景场景一:脚本解释器 / 动态语言运行时实现一个简单的脚本语言时,需要频繁创建和销毁各种运行时对象(字符串、列表、闭包等)。tgc 可以完全接管这些对象的生命周期管理:
#include "tgc.h"#include#includestatic tgc_t gc;typedef enum { TYPE_INT, TYPE_STR } ValType;typedef struct {ValType type;union {int ival;char *sval;} Value;void str_dtor(void *p) {// 字符串缓冲区由 GC 管理,这里做其他清理printf("释放 Value 对象\n");Value *make_int(int n) {Value *v = tgc_alloc_opt(&gc, sizeof(Value), 0, str_dtor);v->type = TYPE_INT;v->ival = n;return v;Value *make_str(const char *s) {Value *v = tgc_alloc_opt(&gc, sizeof(Value), 0, str_dtor);v->type = TYPE_STR;v->sval = tgc_alloc_opt(&gc, strlen(s) + 1, TGC_LEAF, NULL);strcpy(v->sval, s);return v;void interpreter_demo(void) {Value *a = make_int(42);Value *b = make_str("hello, tgc!");printf("int=%d str=%s\n", a->ival, b->sval);// 函数退出后 a、b 不可达,GC 自动回收int main(int argc, char **argv) {tgc_start(&gc, &argc);interpreter_demo();tgc_run(&gc);tgc_stop(&gc);return 0;场景二:原型快速开发在算法原型验证阶段,不想花时间处理内存管理,tgc 可以让你像写 Python 一样写 C:
#include "tgc.h"#includestatic tgc_t gc;int *fibonacci(int n) {int *fib = tgc_calloc(&gc, n, sizeof(int));fib[0] = 0; fib[1] = 1;for (int i = 2; i < n; i++)fib[i] = fib[i-1] + fib[i-2];return fib;void print_fib(int n) {int *f = fibonacci(n);for (int i = 0; i < n; i++)printf("%d ", f[i]);printf("\n");// f 在此函数退出后自动回收int main(int argc, char **argv) {tgc_start(&gc, &argc);print_fib(10);tgc_stop(&gc);return 0;场景三:嵌入式系统资源管理在资源受限的嵌入式环境中,tgc 的轻量特性(仅两个文件)使其易于集成,析构器功能则可用于自动释放外设资源:
#include "tgc.h"#includestatic tgc_t gc;typedef struct {int gpio_pin;int is_open;} GPIOHandle;void gpio_dtor(void *p) {GPIOHandle *h = (GPIOHandle *)p;if (h->is_open) {printf("自动释放 GPIO 引脚 %d\n", h->gpio_pin);h->is_open = 0;// gpio_close(h->gpio_pin); // 实际硬件调用GPIOHandle *open_gpio(int pin) {GPIOHandle *h = tgc_alloc_opt(&gc, sizeof(GPIOHandle), 0, gpio_dtor);h->gpio_pin = pin;h->is_open = 1;printf("打开 GPIO 引脚 %d\n", pin);return h;void gpio_task(void) {GPIOHandle *led = open_gpio(13);GPIOHandle *btn = open_gpio(5);// 使用 led 和 btn ...(void)led; (void)btn;// 函数退出后,GPIO 引脚自动释放int main(int argc, char **argv) {tgc_start(&gc, &argc);gpio_task();tgc_run(&gc);tgc_stop(&gc);return 0;七、常见问题与注意事项Q:指针增量(pointer arithmetic)导致内存提前释放?
tgc 要求内存块的起始地址始终可达。如果只保留了偏移后的指针,GC 会误判为不可达并释放。解决方法是改用整数索引:
// ❌ 危险:x 增量后指向内部,GC 可能提前释放void bad(char *y) {char *x = tgc_alloc(&gc, strlen(y) + 1);while (*x) { x++; } // x 不再指向起始地址// ✅ 正确:始终保留起始地址,用索引遍历void good(char *y) {char *x = tgc_alloc(&gc, strlen(y) + 1);for (int i = 0; x[i]; i++) { /* 处理 x[i] */ }Q:Valgrind 报 uninitialized value 警告?
这是正常现象。GC 扫描栈内存时会读到未初始化的值(这些值只是被检查是否像指针,不会被使用)。使用 --undef-value-errors=no 选项屏蔽此类警告:
valgrind --undef-value-errors=no ./your_programQ:setjmp/longjmp 与 tgc 兼容吗?
不完全兼容,非标准栈行为会破坏 GC 扫描。建议在使用这两个函数期间暂停 GC:
tgc_pause(&gc);// ... setjmp/longjmp 相关代码 ...tgc_resume(&gc);八、社区与生态tgc 是 libcello 项目(同一作者的 C 面向对象框架)中 GC 模块的独立抽象版本。tgc 本身定位为教学和实用的极简 GC 实现,社区规模不大但代码质量高,在 GitHub 上获得了 1.1k+ Star。
与相关项目的对比:
特性
tgc
Boehm GC
MPS
代码量
~500 行
数万行
数十万行
集成难度
极低(2文件)
中等
性能
良好
优秀
卓越
析构器支持
适用场景
原型/小型项目
通用C/C++
高性能运行时
如果你的项目需要工业级 GC,可以考虑 Boehm-Demers-Weiser GC;如果只是想快速消灭内存泄漏、写出更干净的 C 代码原型,tgc 是无与伦比的选择。
九、总结
tgc 用 500 行代码证明了一件事:自动内存管理不是高级语言的专利,C 也可以。它没有任何黑魔法,只有扎实的工程实现——扫描栈、标记堆、清除垃圾,清晰、透明、可理解。
对于以下场景,tgc 是首选:
- 快速验证算法原型,不想手动管理内存
- 构建小型解释器或 DSL 运行时
- 嵌入式项目中需要轻量 GC + 析构器
- 学习垃圾回收器的实现原理
使用时只需记住几个核心原则:始终保持对分配起始地址的引用;对大型无指针数据使用 TGC_LEAF;对全局/静态指向的对象使用 TGC_ROOT;在开启编译优化时通过 volatile 函数指针防内联。
一句话评价:tgc 是学习 GC 原理最好的代码,也是 C 项目告别内存泄漏最轻量的武器。
参考资料:https://github.com/orangeduck/tgc
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.