高级语言 GC 设计全覆盖检查清单(工业级·静态编译型)
适用范围:基于 LLVM 后端、类 Go/Java/C# 风格的静态类型编译语言;可作为 GC 系统设计规范、实现验收标准、线上问题排查依据。
一、核心设计哲学(先定原则,再写代码)
- 可追踪性原则:所有 GC 托管对象必须从已知根出发可达,不允许“隐形对象”。
- 边界清晰原则:明确区分值类型 / 引用类型 / 原生指针,三者生命周期规则完全隔离。
- 统一收口原则:所有 GC 行为只通过固定入口发出,绝不散写在语法、控制流、表达式各处。
- 作用域优先原则:栈对象生命周期由作用域决定,不是由人工 push/pop 决定。
- 不变量原则:任何时刻 GC 都能安全停顿并枚举根集合,不会出现半合法状态。
二、基础模型定义(先把概念定死,GC 就不会乱)
必须在类型系统中定义以下不可变属性:
- GcManaged(是否 GC 托管)
- 对象、字符串、数组、闭包 env、接口装箱 →
true - i32/f64/bool/原生指针/纯值结构体 →
false
- 对象、字符串、数组、闭包 env、接口装箱 →
- IsReference(是否为引用):能为 null、能指向 GC 堆 → 是引用
- IsValueType(是否纯值):复制即完整独立,不影响 GC 堆
- IsUnsafePointer(是否原生裸指针):完全不参与 GC,不做根、不追踪、不移动
只要类型属性明确,90% 的 GC 混乱自动消失。
三、精确 GC 与对象布局规则(LLVM 精确式 GC 必备)
核心规则
- 指针无歧义原则:运行时必须 100% 准确判断一块内存里哪些字段是 GC 引用,哪些是值类型。
- 对象头不变性:所有 GC 托管对象必须有统一对象头,包含元数据指针、标记位、转发指针等。
- 栈槽可区分原则:栈上每个槽位在安全点处必须能判定是否为 GC 指针。
全覆盖检查项
- 所有 GC 对象有统一对象头,元数据中包含字段引用位图(Reference Map)
- 值类型结构体、原生指针、整数不会被误判为 GC 引用
- 数组对象有明确的元素类型标记,可批量判定元素是否为引用类型
- 接口装箱、泛型实例化后,引用位图正确生成,不丢失引用字段
- 栈帧的 Stack Map 精确到字节偏移,无歧义、无重叠
- 全局变量的引用位图在编译期生成,程序启动时统一注册
- 闭包 env 的引用位图随闭包类型自动生成,闭包捕获变量全部可追踪
- 对象对齐填充字节不含伪指针,不干扰 GC 扫描
四、根枚举全覆盖规则(所有根必须被覆盖)
4.1 栈根(Stack Roots)
必须覆盖:
- 函数参数
- 局部变量(let/var)
- 循环变量(for/while 声明的变量)
- catch 异常变量
- 模式匹配绑定变量(when / match)
- 闭包环境指针(env)
规则:
- 进入作用域 → push root
- 离开作用域 → pop root
- 离开方式包括:正常结束、break、continue、return、throw、goto
4.2 临时根(Temporary Roots)【最容易崩】
覆盖所有无名字、无绑定、只在表达式中存活的对象:
- if / while / for / when 条件内临时对象
- 函数调用参数列表中的临时对象
- 链式调用中间节点(a.b().c())
- 运算符 && / || / ?: 内部临时对象
- 数组/对象字面量内部构造临时对象
- return 表达式返回的临时对象
规则:
- 进入表达式 → 加入临时根集合
- 表达式所在语句结束 → 一次性清空
- return 表达式不清空,交给调用方清空
4.3 全局根(Global Roots)
- 全局变量
- 静态字段
- 运行时全局状态
规则:
- 程序启动时注册一次
- 常驻 GC 根集合,不动态增减
4.4 跨调用根(Cross-call Roots)
- 函数返回值(返回 GC 对象时)
- 异步任务上下文(future/promise/协程栈)
- 信号/回调上下文
规则:
- 返回值必须在调用方语句中被接管
- 异步栈必须完整注册为 GC 根
五、赋值与写屏障全覆盖规则
任何引用写入内存,必须走写屏障。
覆盖场景
- 局部变量赋值
x = obj - 字段赋值
obj.f = y - 数组元素赋值
arr[i] = y - 闭包环境捕获赋值
- 接口/装箱赋值
- 容器插入(List/Map/Set)
- 运行时内部操作(string append、substring 等)
屏障规则
- 旧引用先记录 → 新引用写入 → 屏障完成
- 值类型写入不触发屏障
- null 赋值必须触发屏障(移除引用)
- 写屏障是语义边界,不可被编译优化消除、重排、拆分
六、控制流全覆盖规则(所有跳转都不能漏根)
- if / else:条件临时根在 condBr 后清空
- while:循环条件根不泄漏、不累积
- for:init/cond/step 都使用临时根
- when / match:匹配表达式根不跨分支
- break / continue:作用域栈正确回退
- return:返回对象根不提前销毁
- && / || 短路:分支间临时根隔离
- try / catch / finally:异常展开不泄漏根
- 标签跳转 / goto:栈根状态一致
- 无限循环:临时根不无限累积
核心准则
控制流只负责跳转,不负责 GC 逻辑;
GC 只通过作用域与表达式自动管理。
七、闭包与高阶对象全覆盖规则
- 自由变量捕获 → 写屏障
- 闭包环境 env → gcAlloc 分配
- env 内部修改 → 写屏障
- 嵌套闭包 → 多层 env 都正确追踪
- 禁止临时对象被捕获后逃逸作用域
八、字面量与复合对象全覆盖规则
- 字符串字面量 → GC 分配
- 数组字面量 → 元素写屏障
- 对象字面量 → 字段写屏障
- 模板字符串 → 新对象 GC 分配
- 集合字面量 → 插入屏障
九、函数调用与 ABI 安全规则
- 实参全部生成完毕 → 统一加入临时根
- 调用指令之后 → 清空参数临时根
- 被调用函数自己管理参数栈根
- 调用 GC runtime 函数不重复埋点
- FFI 外部函数必须声明是否返回 GC 对象
- 尾调用优化(TCO)不破坏栈帧根枚举,尾调用前清空本帧所有临时根
- 函数内联后,内层作用域的栈根依然能被正确 push/pop,不出现作用域错位
十、异常与栈展开(Unwind)全覆盖规则
- throw 对象 → 正常临时根管理
- 栈展开时按调用栈批量 pop 根
- catch 变量作为局部变量入根
- finally 不破坏栈结构
- 异常不泄漏临时根
- 构造函数抛异常:半初始化对象的已分配子对象能被正确回收,无内存泄漏
- 栈溢出异常抛出时,当前栈帧的根状态一致,不导致 GC 扫描崩溃
十一、GC 安全点规则
插入位置(只在以下位置插入,不乱插)
- 函数入口
- 循环回跳点(while/for 顶部)
- 函数调用前/后
- 显式 yield / await
- 长时间循环步进点
- 超长直线代码路径插入强制安全点
安全点行为约束
- 安全点处生成完整的栈映射表(Stack Map / Statepoint)
- 安全点内禁止分配、禁止修改引用字段
- 安全点不可重入:安全点内不能再次触发 GC
- 所有循环必须有安全点(无论循环体多小)
- 原子操作、无锁代码路径也必须有可到达的安全点
禁止位置
- 表达式内部
- 字段访问
- 算术运算
- 基本块中间
十二、编译优化与 GC 交互约束(LLVM 必踩坑)
核心规则
- 安全点状态一致性:GC 只能在安全点触发,安全点处所有 GC 根必须可精确枚举。
- 存活期边界约束:对象的 GC 存活期以语言层面作用域为准,不能被编译器优化提前终止。
- 写屏障不可消除:写屏障是语义边界,不能被死代码消除、指令重排破坏。
全覆盖检查项
- 所有安全点处生成完整栈映射表,精确标记每个栈槽、寄存器是否为 GC 指针
- 禁止编译器在安全点之间收缩 GC 对象的存活范围
- 写屏障函数标记为 NoInline / 强内存屏障,防止被重排、消除、拆分
- 循环不变量外提(LICM)不会把 GC 对象分配提到循环外,导致生命周期异常延长
- 死代码消除(DCE)、冗余存储消除不会误删写屏障调用
- 寄存器分配后,安全点处所有存活 GC 指针必须有栈槽备份,不能只存在寄存器中
十三、并发与多线程 GC 规则
核心规则
- 全局安全点协议:GC 触发时,所有线程必须在有限时间内到达安全点并停顿。
- 跨线程写屏障一致性:堆上引用的修改必须经过写屏障,无论哪个线程执行。
- 线程根独立原则:每个线程的栈、TLS、寄存器都是独立根集合,互不干扰。
全覆盖检查项
- 每个线程有独立的栈根、临时根、作用域栈,线程间不共享根集合
- 线程启动时注册到 GC 线程列表,退出时注销并清理所有根
- 线程本地存储(TLS)中的 GC 对象被正确枚举为根
- 全局安全点机制:GC 触发后,所有线程在安全点处挂起,无遗漏
- 并发写屏障(SATB 或增量更新)正确实现,无数据竞争、无漏标
- 锁对象本身是 GC 根,锁持有期间对象不被回收
- 线程池、协程调度器上下文切换时,正确保存/恢复 GC 根状态
- 禁止在安全点之外长时间阻塞原生代码(导致 GC 停顿超时)
十四、FFI 与原生代码交互规则
核心规则
- 原生代码不裸持 GC 指针:必须通过句柄(Handle)间接持有。
- 原生代码边界即安全点边界:进入/离开原生代码时必须更新根集合状态。
全覆盖检查项
- 定义局部句柄:原生函数调用期间有效,调用结束自动批量释放
- 定义全局句柄:跨调用、跨线程长期有效,显式创建/销毁
- 原生回调进入语言侧时,自动创建句柄作用域,退出时清理
- 原生代码中发生 GC 时,所有句柄都被枚举为根
- FFI 传入的原生指针标记为非 GC 托管,不追踪、不扫描
- 原生代码调用 GC 运行时函数符合安全点语义
- 原生堆和 GC 堆严格分离,原生堆不嵌入 GC 指针
十五、弱引用 / 软引用 / 终结器体系规则
核心规则
- 可达性分级原则:强引用 > 软引用 > 弱引用 > 虚引用,GC 按级别依次回收。
- 终结器单次执行原则:每个对象的终结器最多执行一次,执行后对象不可复活。
- 引用队列原子性:引用入队和对象回收是原子操作。
全覆盖检查项
- 弱引用对象本身是 GC 托管对象,指向的 referent 有特殊标记
- GC 标记阶段判定对象只有弱引用可达时,置空弱引用并加入引用队列
- 终结器对象在回收前执行,执行期间对象临时复活,执行后不可再次复活
- 软引用在内存不足时回收,有明确的回收策略和优先级
- 循环引用中的弱引用能正确打破循环,不导致内存泄漏
- 终结器中抛出异常不影响 GC 整体流程,有兜底异常捕获
十六、极端边界与异常路径规则
- OOM 兜底机制:分配失败时自动触发 Full GC,重试分配;再次失败才抛出 OOM
- 递归深度过大:栈根数量不溢出,GC 能正确遍历深层调用栈
- 空指针解引用前:指向的对象仍然存活,不被优化提前回收
- 运行时内部错误:GC 自身异常不会破坏堆结构,有最小化安全退出路径
- 大对象分配:超过阈值的对象走大对象专属分配区,不污染分代、不造成碎片化
- 线程退出后,其栈上引用的对象能被正确回收,无内存泄漏
十七、可观测性与正确性校验规则
- GC 前后可开启堆完整性校验:验证所有对象头、标记位、引用合法性
- 根枚举完整性校验:作用域退出时检查栈根数量匹配,无泄漏、无多 pop
- 写屏障断言:Debug 模式下修改引用字段时断言已经过写屏障
- 可配置 GC 日志:每次 GC 的原因、耗时、回收量、各代大小、停顿时间
- 可统计 GC 指标:分配速率、回收速率、安全点等待时间、分代晋升速率
- Debug 模式支持:强制 GC、重复 GC、扫描时检测野指针
- 崩溃时现场 dump:GC 崩溃时输出当前堆状态、根集合、安全点位置
十八、异常诊断与堆栈溯源规则
核心原则
- 任何异常必须可定位到源码:所有崩溃、GC错误、运行时异常,最终都要能映射到语言层面的函数名、文件与行号。
- 异常现场最小化保留:崩溃时优先输出关键上下文,不输出无意义的海量日志,但核心定位信息一项不能少。
- 分层展示:区分「用户代码栈帧」和「运行时/GC内部栈帧」,默认优先展示业务层,排查GC问题时可展开内部细节。
18.1 通用异常堆栈基础能力
- 所有语言层面异常抛出时,自动收集完整调用栈,包含:函数名、源码文件路径、行号、列号
- 栈帧符号基于调试元数据(DWARF / CodeView)生成,Release 模式可保留分离符号文件,不影响发布体积
- 内联函数可展开完整调用链,标注内联发生的位置,不丢失上层调用上下文
- lambda、闭包、泛型实例化的栈帧,可映射回源码定义位置,不显示编译器内部混淆名称
- 异常栈信息可被程序捕获、序列化、打印,也可在崩溃时自动输出到标准错误/日志文件
- 栈展开过程中,GC 根状态保持一致,不会因收集堆栈导致二次崩溃
18.2 GC 高频异常专项日志
① 内存不足(OOM)
- 异常信息必须包含:请求分配的大小、当前堆总大小、已用大小、剩余可用空间
- 附带 GC 上下文:最近 5 次 GC 的触发原因、回收量、耗时、分代大小变化
- 记录触发分配的源码位置:文件、行号、分配的对象类型
- 可选输出:大对象统计、内存泄漏热点 Top N 类型
② 野指针 / 非法引用访问
- 记录非法指针地址、指向的内存区域属性(已释放堆块、栈内存、未映射页)
- 若指向已释放对象:输出对象类型、释放时间、释放位置的调用栈、对象原始分配栈
- 记录访问该指针的源码位置:字段访问、数组下标、函数调用点
- 附带最近一次 GC 的时间与阶段,判断是否为 GC 回收后误用
③ 堆结构损坏
- 定位损坏的内存地址,输出损坏位置前后的原始内存内容
- 识别损坏区域所属的对象,输出对象元数据、引用位图、所属分代
- 记录最后一次修改该内存区域的线程 ID 与操作类型(写入、分配、回收)
- 标注损坏发生在 GC 的哪个阶段(标记、清理、压缩、移动)
④ GC 安全点超时 / 停顿过长
- 列出所有未到达安全点的线程 ID、当前执行指令地址(PC)、对应源码位置
- 标注线程是否阻塞在原生代码(FFI 调用)、是否持有锁
- 输出本次 GC 触发原因、已停顿时间、各阶段耗时分布
- 记录安全点数量最多的代码位置,辅助排查循环无安全点问题
⑤ 句柄泄漏 / 溢出(FFI 场景)
- 输出当前句柄总数、局部句柄/全局句柄分布
- 记录创建次数最多的 Top N 调用栈,快速定位泄漏点
- 句柄溢出时自动触发 GC 并输出完整句柄列表快照
18.3 崩溃现场保留规则
- 程序崩溃(信号/异常终止)时,自动生成最小化崩溃报告:
- 异常类型与错误码
- 崩溃线程的完整调用栈(源码级)
- 崩溃指令地址 + 对应源码位置
- 当前 GC 状态(是否正在 GC、处于哪个阶段)
- 堆内存摘要:总大小、已用、对象数量 Top 类型
- 可配置生成完整核心转储(Core Dump),默认关闭但保留开关
- Debug 模式下崩溃时自动触发堆完整性校验,输出所有检测到的非法引用
- 所有异常日志统一格式:时间戳、线程ID、异常等级、错误码、定位信息、上下文摘要
18.4 可观测性辅助能力
- 支持「GC 诊断模式」开关,开启后输出:每次安全点位置、每次写屏障、每次根枚举细节
- 提供内置工具:可根据地址反查对象类型、分配栈、引用关系
- 支持符号服务器/分离符号文件,线上 Release 包无需带符号也能定位到源码行
- 异常栈可过滤运行时内部帧,默认只显示用户代码栈,排查问题时可展开全部
十九、禁止性规则总览(GC 乱一定是违反了这些)
- ❌ 不许在表达式/语句中直接手写 gc_push / gc_pop
- ❌ 不许跨基本块保留临时对象
- ❌ 不许在循环条件不清空临时根
- ❌ 不许丢失泛型类型的 GC 标记
- ❌ 不许把裸指针当作 GC 引用
- ❌ 不许在写屏障前覆盖旧引用
- ❌ 不许 return 之后再 clear 临时根
- ❌ 不许闭包捕获临时对象并逃逸作用域
- ❌ 不许在安全点之外触发 GC
- ❌ 不许原生代码裸持 GC 对象指针长期存活
- ❌ 不许编译优化消除写屏障调用
- ❌ 不许终结器中复活对象并逃逸到外部强引用
- ❌ 不许在无引用位图的情况下扫描堆/栈
- ❌ 不许在多线程下无写屏障修改共享引用
- ❌ 不许用“看着像指针就当根”的保守扫描作为正式方案
- ❌ 不许在对象布局中出现“半指针半值”的联合类型而不做标记
- ❌ 不许异常发生时只输出原生栈帧,不映射回语言层面源码位置
- ❌ 不许 GC 崩溃时无任何现场信息直接退出,至少保留崩溃地址与栈回溯
- ❌ 不许 Release 模式完全丢弃符号映射关系,导致线上问题无法定位
- ❌ 不许异常栈展开过程中修改 GC 根集合状态,导致二次崩溃
- ❌ 不许在异常处理路径中执行新的 GC 分配,避免嵌套异常
二十、完备性验证测试集
全部通过 = GC 设计全覆盖、生产级可用
基础正确性
while (obj.child() != null) { }不崩溃f(a(), b(), c())参数临时对象不被回收if (a() && b() || c())短路不丢根return obj.child()返回值正常存活[a(), b(), c()]数组元素都存活() => x闭包捕获不野指针try { throw obj } catch (e) {}异常对象存活- 无限循环不导致根无限增长
- 链式调用
a.b().c().d中间对象不消失 - 泛型容器存储对象不丢屏障、不提前回收
进阶边界
- 开启 O2 优化后,以上所有用例全部通过,无随机崩溃
- 多线程并发修改同一对象,GC 并行标记不崩溃、不丢对象
- FFI 原生代码持有对象句柄,GC 移动后指针仍然有效
- 构造函数中途抛异常,所有已分配子对象正确回收,无泄漏
- 弱引用指向的对象在 GC 后被正确置空,不出现野指针
- 大对象数组分配与回收正常,不产生不可用碎片
- 线程退出后,其栈引用的对象能被正确回收,无内存泄漏
- OOM 触发时程序优雅抛出异常,不堆损坏、不崩溃
诊断定位
- 抛出未捕获异常时,输出包含源码文件名和行号的完整调用栈
- OOM 异常日志包含分配大小、堆状态、触发位置三类核心信息
- 非法内存访问崩溃时,可定位到触发的源码行和对应的对象类型
- Release 模式下通过分离符号文件,仍可将崩溃地址映射回源码行号
- 构造函数抛异常时,堆栈可追溯到具体构造调用点,而非只显示运行时内部帧
- GC 安全点超时异常可定位到具体哪个线程、哪段代码未到达安全点
二十一、终极设计方法论
值类型不管,引用类型必追踪;
具名变量走作用域,临时对象走表达式;
内存写入必屏障,对象分配统一管;
控制流只管跳转,GC 绝不散落在各处。