高级语言 GC 设计全覆盖检查清单(工业级·静态编译型)

适用范围:基于 LLVM 后端、类 Go/Java/C# 风格的静态类型编译语言;可作为 GC 系统设计规范、实现验收标准、线上问题排查依据。


一、核心设计哲学(先定原则,再写代码)

  1. 可追踪性原则:所有 GC 托管对象必须从已知根出发可达,不允许“隐形对象”。
  2. 边界清晰原则:明确区分值类型 / 引用类型 / 原生指针,三者生命周期规则完全隔离。
  3. 统一收口原则:所有 GC 行为只通过固定入口发出,绝不散写在语法、控制流、表达式各处。
  4. 作用域优先原则:栈对象生命周期由作用域决定,不是由人工 push/pop 决定。
  5. 不变量原则:任何时刻 GC 都能安全停顿并枚举根集合,不会出现半合法状态。

二、基础模型定义(先把概念定死,GC 就不会乱)

必须在类型系统中定义以下不可变属性:

  1. GcManaged(是否 GC 托管)
    • 对象、字符串、数组、闭包 env、接口装箱 → true
    • i32/f64/bool/原生指针/纯值结构体 → false
  2. IsReference(是否为引用):能为 null、能指向 GC 堆 → 是引用
  3. IsValueType(是否纯值):复制即完整独立,不影响 GC 堆
  4. IsUnsafePointer(是否原生裸指针):完全不参与 GC,不做根、不追踪、不移动

只要类型属性明确,90% 的 GC 混乱自动消失。


三、精确 GC 与对象布局规则(LLVM 精确式 GC 必备)

核心规则

  1. 指针无歧义原则:运行时必须 100% 准确判断一块内存里哪些字段是 GC 引用,哪些是值类型。
  2. 对象头不变性:所有 GC 托管对象必须有统一对象头,包含元数据指针、标记位、转发指针等。
  3. 栈槽可区分原则:栈上每个槽位在安全点处必须能判定是否为 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 必踩坑)

核心规则

  1. 安全点状态一致性:GC 只能在安全点触发,安全点处所有 GC 根必须可精确枚举。
  2. 存活期边界约束:对象的 GC 存活期以语言层面作用域为准,不能被编译器优化提前终止。
  3. 写屏障不可消除:写屏障是语义边界,不能被死代码消除、指令重排破坏。

全覆盖检查项

  • 所有安全点处生成完整栈映射表,精确标记每个栈槽、寄存器是否为 GC 指针
  • 禁止编译器在安全点之间收缩 GC 对象的存活范围
  • 写屏障函数标记为 NoInline / 强内存屏障,防止被重排、消除、拆分
  • 循环不变量外提(LICM)不会把 GC 对象分配提到循环外,导致生命周期异常延长
  • 死代码消除(DCE)、冗余存储消除不会误删写屏障调用
  • 寄存器分配后,安全点处所有存活 GC 指针必须有栈槽备份,不能只存在寄存器中

十三、并发与多线程 GC 规则

核心规则

  1. 全局安全点协议:GC 触发时,所有线程必须在有限时间内到达安全点并停顿。
  2. 跨线程写屏障一致性:堆上引用的修改必须经过写屏障,无论哪个线程执行。
  3. 线程根独立原则:每个线程的栈、TLS、寄存器都是独立根集合,互不干扰。

全覆盖检查项

  • 每个线程有独立的栈根、临时根、作用域栈,线程间不共享根集合
  • 线程启动时注册到 GC 线程列表,退出时注销并清理所有根
  • 线程本地存储(TLS)中的 GC 对象被正确枚举为根
  • 全局安全点机制:GC 触发后,所有线程在安全点处挂起,无遗漏
  • 并发写屏障(SATB 或增量更新)正确实现,无数据竞争、无漏标
  • 锁对象本身是 GC 根,锁持有期间对象不被回收
  • 线程池、协程调度器上下文切换时,正确保存/恢复 GC 根状态
  • 禁止在安全点之外长时间阻塞原生代码(导致 GC 停顿超时)

十四、FFI 与原生代码交互规则

核心规则

  1. 原生代码不裸持 GC 指针:必须通过句柄(Handle)间接持有。
  2. 原生代码边界即安全点边界:进入/离开原生代码时必须更新根集合状态。

全覆盖检查项

  • 定义局部句柄:原生函数调用期间有效,调用结束自动批量释放
  • 定义全局句柄:跨调用、跨线程长期有效,显式创建/销毁
  • 原生回调进入语言侧时,自动创建句柄作用域,退出时清理
  • 原生代码中发生 GC 时,所有句柄都被枚举为根
  • FFI 传入的原生指针标记为非 GC 托管,不追踪、不扫描
  • 原生代码调用 GC 运行时函数符合安全点语义
  • 原生堆和 GC 堆严格分离,原生堆不嵌入 GC 指针

十五、弱引用 / 软引用 / 终结器体系规则

核心规则

  1. 可达性分级原则:强引用 > 软引用 > 弱引用 > 虚引用,GC 按级别依次回收。
  2. 终结器单次执行原则:每个对象的终结器最多执行一次,执行后对象不可复活。
  3. 引用队列原子性:引用入队和对象回收是原子操作。

全覆盖检查项

  • 弱引用对象本身是 GC 托管对象,指向的 referent 有特殊标记
  • GC 标记阶段判定对象只有弱引用可达时,置空弱引用并加入引用队列
  • 终结器对象在回收前执行,执行期间对象临时复活,执行后不可再次复活
  • 软引用在内存不足时回收,有明确的回收策略和优先级
  • 循环引用中的弱引用能正确打破循环,不导致内存泄漏
  • 终结器中抛出异常不影响 GC 整体流程,有兜底异常捕获

十六、极端边界与异常路径规则

  • OOM 兜底机制:分配失败时自动触发 Full GC,重试分配;再次失败才抛出 OOM
  • 递归深度过大:栈根数量不溢出,GC 能正确遍历深层调用栈
  • 空指针解引用前:指向的对象仍然存活,不被优化提前回收
  • 运行时内部错误:GC 自身异常不会破坏堆结构,有最小化安全退出路径
  • 大对象分配:超过阈值的对象走大对象专属分配区,不污染分代、不造成碎片化
  • 线程退出后,其栈上引用的对象能被正确回收,无内存泄漏

十七、可观测性与正确性校验规则

  • GC 前后可开启堆完整性校验:验证所有对象头、标记位、引用合法性
  • 根枚举完整性校验:作用域退出时检查栈根数量匹配,无泄漏、无多 pop
  • 写屏障断言:Debug 模式下修改引用字段时断言已经过写屏障
  • 可配置 GC 日志:每次 GC 的原因、耗时、回收量、各代大小、停顿时间
  • 可统计 GC 指标:分配速率、回收速率、安全点等待时间、分代晋升速率
  • Debug 模式支持:强制 GC、重复 GC、扫描时检测野指针
  • 崩溃时现场 dump:GC 崩溃时输出当前堆状态、根集合、安全点位置

十八、异常诊断与堆栈溯源规则

核心原则

  1. 任何异常必须可定位到源码:所有崩溃、GC错误、运行时异常,最终都要能映射到语言层面的函数名、文件与行号。
  2. 异常现场最小化保留:崩溃时优先输出关键上下文,不输出无意义的海量日志,但核心定位信息一项不能少。
  3. 分层展示:区分「用户代码栈帧」和「运行时/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 崩溃现场保留规则

  • 程序崩溃(信号/异常终止)时,自动生成最小化崩溃报告:
    1. 异常类型与错误码
    2. 崩溃线程的完整调用栈(源码级)
    3. 崩溃指令地址 + 对应源码位置
    4. 当前 GC 状态(是否正在 GC、处于哪个阶段)
    5. 堆内存摘要:总大小、已用、对象数量 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 设计全覆盖、生产级可用

基础正确性

  1. while (obj.child() != null) { } 不崩溃
  2. f(a(), b(), c()) 参数临时对象不被回收
  3. if (a() && b() || c()) 短路不丢根
  4. return obj.child() 返回值正常存活
  5. [a(), b(), c()] 数组元素都存活
  6. () => x 闭包捕获不野指针
  7. try { throw obj } catch (e) {} 异常对象存活
  8. 无限循环不导致根无限增长
  9. 链式调用 a.b().c().d 中间对象不消失
  10. 泛型容器存储对象不丢屏障、不提前回收

进阶边界

  1. 开启 O2 优化后,以上所有用例全部通过,无随机崩溃
  2. 多线程并发修改同一对象,GC 并行标记不崩溃、不丢对象
  3. FFI 原生代码持有对象句柄,GC 移动后指针仍然有效
  4. 构造函数中途抛异常,所有已分配子对象正确回收,无泄漏
  5. 弱引用指向的对象在 GC 后被正确置空,不出现野指针
  6. 大对象数组分配与回收正常,不产生不可用碎片
  7. 线程退出后,其栈引用的对象能被正确回收,无内存泄漏
  8. OOM 触发时程序优雅抛出异常,不堆损坏、不崩溃

诊断定位

  1. 抛出未捕获异常时,输出包含源码文件名和行号的完整调用栈
  2. OOM 异常日志包含分配大小、堆状态、触发位置三类核心信息
  3. 非法内存访问崩溃时,可定位到触发的源码行和对应的对象类型
  4. Release 模式下通过分离符号文件,仍可将崩溃地址映射回源码行号
  5. 构造函数抛异常时,堆栈可追溯到具体构造调用点,而非只显示运行时内部帧
  6. GC 安全点超时异常可定位到具体哪个线程、哪段代码未到达安全点

二十一、终极设计方法论

值类型不管,引用类型必追踪;
具名变量走作用域,临时对象走表达式;
内存写入必屏障,对象分配统一管;
控制流只管跳转,GC 绝不散落在各处。