第 30 章 / 共 51 章
卷四 · 热重载深潜

GC 与零泄漏:旧代的安葬仪式

lease 归零之后发生什么?一条专职线程按固定顺序把旧代拆干净——这一章讲"确认它真的拆干净了"的方法。

本章导读

站位:卷四的验证章——机制前面讲完了,本章给你观测手段(finalize 日志、force-free 断言、内存报告)与一个真实的历史案例(早期"旧代全泄漏策略"如何演进为零泄漏 GC)。

30.1 引入

热重载的垃圾回收有个天然难题:旧代的死活取决于异步事件的完成时序(连接什么时候断、回调什么时候返回),多 worker 环境下不可预测。XServer 的发展史上真实出现过两种策略:早期版本选择"旧代全泄漏"(不释放,保绝对安全);现行版本进化为"引用归零 + 安全信号 + 延迟释放"——安全信号是旧驱动连接计数归零(所有 Close 回调完成),归零并过宽限后按序释放。演进的原因很简单:泄漏策略下频繁重载会吃光内存。

30.2 reaper 与析构顺序

generation reaper(专职线程,引擎定时器 500ms 扫描) 检测:旧代 lease == 0(安全信号:旧驱动连接计数归零) │ + 宽限期(custom/udp 类 5s 固定;30s 超时兜底) ▼ 按序析构(顺序固定) 1. ServiceUnit /* 你的清理:关 db、刷缓冲 */ 2. TCC 状态 /* 编译产物与符号 */ 3. 驱动 runtime /* 协议层状态 */ 4. server 结构 /* 配置树与快照 */ 日志:generation finalized(每代一次) 断言基线:全程无 force-free / 无 timeout force

顺序的含义:你的 ServiceUnit 跑在一切宿主资源拆除之前——清理时宿主设施仍可用;而它又跑在旧代已经不再服务之后——清理不会影响任何请求。这个位置是设计出来的。

30.3 实测案例

仓库历史里记录着这个机制的验收实验(三循环 8080↔8081 换代):同一组端口反复换代三轮,断言——

  • 每轮服务全通(请求零失败)
  • 6 个旧代全部 finalize(日志计数对齐换代次数)
  • 零泄漏(内存报告前后一致)、零崩溃、无 force-free

你自己的服务可以用同样的三段式验收:压测流量下 N 次换代 → 日志 N 次 finalized → RSS 曲线平稳(drill 的采样方式)。这套断言就固化在 lifecycle_reload_test 与 reload_matrix 里(ch06 的基线表)。

30.4 定时器随代作废

GC 顺序里藏着一条对应用重要的推论:旧代的定时器在换代时作废,而不是等 GC。机制上,定时器绑定创建它的那个代(严格说是"正在执行的旧代"的线程局部标记)——换代后旧代的定时器被取消,新代的定时器从新代 Init 里重新挂。

  • 结果:定时器永远不会打进错误的代——回调里访问的静态变量一定属于创建它的那代
  • 代价:周期任务必须在新代 Init 里重建(ch27 文件监听触发器的坑就是它)
  • 在途的那一次触发:已进入执行的回调由该代 lease 保护到完成

30.5 契约

  • 顺序线:ServiceUnit → TCC → 驱动 → 配置,不可变。
  • 观测线:finalized 日志计数 = 换代次数;force-free 出现即为缺陷。
  • 定时器线:绑定创建代;换代作废;新代 Init 重建周期任务。

常见错误

ServiceUnit 里做长阻塞
症状
换代多代后 GC 队列堆积,finalize 日志滞后于换代。
原因
reaper 线程串行执行各代的 Unit——一个 Unit 卡 30 秒,后面全部排队。清理要快(关句柄、刷缓冲),长活留给后台线程并自行管理。
把 finalize 滞后当泄漏
症状
换代后好几分钟才看到 finalized,怀疑内存泄漏。
原因
长连接(WS/TAKEOVER)持有旧代 lease——finalize 在等它们结束(ch29 的 lease 表)。先查连接,再谈泄漏。

编程练习

基础finalize 计数

连续换代 5 次,统计 finalized 日志数并与次数对账。

验收:5:5 对齐,无 force-free。
进阶三循环验收

复刻 30.3 实验:压测下三循环换代 + RSS 采样曲线。

验收:请求零失败;RSS 稳定在换代前水平 ±5%。
挑战定时器作废实证

旧代挂一个 10s 周期任务(打印代标记);第 3 秒换代,观察旧任务停止、新代重建后继续。

验收:日志能区分两代的打印;旧任务在换代后零次触发。

小结

要点结论
策略演进早期全泄漏 → 引用归零 + 安全信号 + 延迟释放
析构顺序ServiceUnit → TCC → 驱动 → 配置(固定)
观测finalized 计数对账;force-free = 缺陷信号
定时器绑定创建代、换代作废、Init 重建
xrt 延伸阅读