网易首页 > 网易号 > 正文 申请入驻

告别内存泄漏:500行代码实现的C语言垃圾回收器 tgc 深度指南

0
分享至

一、介绍

在 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 等实际项目
三、架构3.1 整体设计

tgc 的架构极为简洁,核心思路是:

  1. 维护所有分配列表:通过 tgc_alloc 系列函数分配内存时,将指针、大小、标志、析构器等元信息记录到内部表中
  2. 扫描可达内存:在 GC 触发时,扫描栈内存和 GC 堆内存,找出所有可达指针
  3. 清除不可达内存:对比两个列表,释放不可达的内存块

┌──────────────────────────────────────────────┐│                    tgc_t                     ││  ┌─────────────┐   ┌──────────────────────┐  ││  │  stack_top  │   │  items[] 分配记录表  │  ││  │  stack_btm  │   │  ptr | size | flags  │  ││  │  paused     │   │  dtor | mark bit     │  ││  └─────────────┘   └──────────────────────┘  │└──────────────────────────────────────────────┘│                    │扫描栈内存            扫描堆内存└──────────┬─────────┘标记可达对象清除不可达对象(调用析构器→释放内存)
3.2 可达性规则

一个内存块被认为可达,当且仅当:

  • 栈上(至少比 tgc_start 调用深一层的函数栈帧中)存在指向它起始地址的指针,或
  • 另一个由 GC 管理的内存块中存在指向它起始地址的指针

不会被识别为可达的情况:

  • 指针指向内存块内部(而非起始地址)
  • 指针位于静态数据段(static 变量)
  • 指针来自非 GC 分配的内存(如原生 malloc)
  • 指针来自其他线程
3.3 GC 触发时机
  • 调用 tgc_alloc 系列函数时,若当前分配数量超过阈值,自动触发一次标记-清除
  • 调用 tgc_run 手动触发
  • 调用 tgc_stop 时,释放所有剩余内存
四、快速上手4.1 集成方式

将 tgc.h 和 tgc.c 复制到项目中,然后一起编译:

gcc your_program.c tgc.c -o your_program
4.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_program

Q: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.

相关推荐
热点推荐
“招商伊敦”号被卖:中国为什么留不住豪华邮轮?

“招商伊敦”号被卖:中国为什么留不住豪华邮轮?

闻旅派
2026-02-27 19:22:08
浅色系穿搭!这个组合让你在健身房瞬间吸引眼球!

浅色系穿搭!这个组合让你在健身房瞬间吸引眼球!

独角showing
2025-12-31 21:08:57
女子谈释永信过往,她们姐妹住少林寺3天2夜,争着往释永信房间跑

女子谈释永信过往,她们姐妹住少林寺3天2夜,争着往释永信房间跑

江山挥笔
2025-07-29 16:50:59
美媒:特朗普与军方就对伊朗行动分歧明显

美媒:特朗普与军方就对伊朗行动分歧明显

界面新闻
2026-02-26 23:35:50
等了四天,中方终于回应特朗普访华,信号很明确

等了四天,中方终于回应特朗普访华,信号很明确

兰妮搞笑分享
2026-02-27 16:58:14
2005年,美国华裔间谍案,完美动力公司工程师连床事都被FBI监控

2005年,美国华裔间谍案,完美动力公司工程师连床事都被FBI监控

干史人
2026-02-23 20:33:25
从 GUCCI 柜姐到豪门女主!乔治娜重返米兰看秀,气场全开

从 GUCCI 柜姐到豪门女主!乔治娜重返米兰看秀,气场全开

述家娱记
2026-02-28 10:26:28
他被开除党籍和公职

他被开除党籍和公职

锡望
2026-02-27 14:34:04
乌克兰逃避兵役现象愈演愈烈

乌克兰逃避兵役现象愈演愈烈

参考消息
2026-02-26 19:51:14
官媒曝光68岁阎维文处境,李双江预言成真

官媒曝光68岁阎维文处境,李双江预言成真

余塩搞笑段子
2026-01-14 13:04:37
“广东人真的太有爱了!”年初一车被卡,路人反应让浙江游客破防|凡人微光

“广东人真的太有爱了!”年初一车被卡,路人反应让浙江游客破防|凡人微光

瓜哥的动物日记
2026-02-27 18:49:19
马筱梅产子后汪小菲首开播,教马筱梅拍嗝,谈及小儿子父爱满满

马筱梅产子后汪小菲首开播,教马筱梅拍嗝,谈及小儿子父爱满满

小徐讲八卦
2026-02-28 06:22:43
马未都:香港宁愿要20万菲佣,也不接受内地保姆,原因很简单

马未都:香港宁愿要20万菲佣,也不接受内地保姆,原因很简单

谈史论天地
2026-02-19 12:44:34
上海女子花8800元雇团队寻猫,结果物业发现就在屋内!寻宠团队:按结果收费不退钱;当事人已报警

上海女子花8800元雇团队寻猫,结果物业发现就在屋内!寻宠团队:按结果收费不退钱;当事人已报警

新民晚报
2026-02-27 19:32:24
伊朗用血泪换来的教训:一旦中美开战,中国必须首先锁定这一点

伊朗用血泪换来的教训:一旦中美开战,中国必须首先锁定这一点

冷峻视角下的世界
2026-02-20 07:45:35
巴基斯坦空袭阿富汗首都!双方为何再次大打出手?

巴基斯坦空袭阿富汗首都!双方为何再次大打出手?

斌闻天下
2026-02-27 12:04:52
万达继续出售资产 20亿元转让上海颛桥万达广场

万达继续出售资产 20亿元转让上海颛桥万达广场

财联社
2026-02-27 12:50:11
俄反对派媒体公布俄军阵亡惊人数据

俄反对派媒体公布俄军阵亡惊人数据

小眼睛小世界
2026-02-27 09:20:27
以色列已经告诉世界:日本若敢拥有核武器,美国并不会第一个翻脸

以色列已经告诉世界:日本若敢拥有核武器,美国并不会第一个翻脸

八斗小先生
2025-12-26 09:33:27
4-0!中国女足又赢了,亚洲杯剑指卫冕,王霜喊话:争取上领奖台

4-0!中国女足又赢了,亚洲杯剑指卫冕,王霜喊话:争取上领奖台

绿茵舞着
2026-02-27 16:51:31
2026-02-28 10:56:49
呼呼历史论
呼呼历史论
分享有趣的历史
405文章数 16480关注度
往期回顾 全部

科技要闻

狂揽1100亿美元!OpenAI再创融资神话

头条要闻

1岁多男童春节探亲鼠药中毒 爸爸:他还没好好看过世界

头条要闻

1岁多男童春节探亲鼠药中毒 爸爸:他还没好好看过世界

体育要闻

球队主力全报销?顶风摆烂演都不演了

娱乐要闻

郭晶晶霍启刚现身香港艺术节尽显恩爱

财经要闻

沈明高提共富建议 百姓持科技股国家兜底

汽车要闻

岚图泰山黑武士版3月上市 搭载华为四激光智驾方案

态度原创

健康
亲子
艺术
游戏
军事航空

转头就晕的耳石症,能开车上班吗?

亲子要闻

被点名的儿童禁用药,家长一定要注意

艺术要闻

这幅草书中19个字,您能一眼看懂吗?“徐娘半老”含义引热议!

IGN再评9分格斗神作!2D格斗天花板绝对夯爆了

军事要闻

美国11架F-22隐形战机抵达以色列

无障碍浏览 进入关怀版