在日常 C++ 开发里,我们从小被教育:
RAII 是王道
new 一定要 delete
对象生命周期必须严谨管理
但当你进入 Chromium / 浏览器内核 / 大型常驻进程系统 的世界后,会发现一件非常“反常识”的事:
❗ 大量对象 new 了故意不 delete
❗ 单例用裸指针 + static
❗ 进程退出时 根本不走析构
很多工程师第一次看到类似代码时都会震惊:
ShortcutKey* ShortcutKey::GetInstance(Profile* profile) { static ShortcutKey* instance = nullptr; if (!instance) instance = new ShortcutKey(profile->GetOriginalProfile()); return instance; }
这不是内存泄漏吗???
答案是:
在浏览器进程里,这是“设计”,不是 Bug。
今天我们就从操作系统层、C++ 运行时层、Chromium 架构层三层,把这件事讲透。
一、进程退出那一刻,操作系统到底干了什么?
先打破第一个误区:
❌ “程序退出时,系统会帮你调用析构函数”
错。操作系统根本不知道什么是 C++。
当浏览器进程结束时(无论是正常关闭还是被 kill),OS 做的事情非常“物理”:
| 操作 | 说明 |
|---|---|
| 回收整块虚拟内存 | 直接把进程地址空间抹掉 |
| 关闭内核对象 | 文件句柄、socket、管道 |
| 停止线程调度 | 所有线程终止 |
它不会做的事情:
| 不会发生 | 原因 |
|---|---|
| 调用类析构函数 | OS 不认识 C++ 对象 |
| 调用 delete | OS 不知道堆分配 |
| 调用 vector / string 析构 | 语言层行为 |
| 调用单例析构 | 静态对象逻辑属于运行时 |
也就是说:
内存被回收 ≠ 对象被析构
二、什么时候析构才会被调用?
析构是否执行,取决于 进程是怎么退出的。
| 退出方式 | 析构是否执行 |
|---|---|
return 0; from main | ✅ 会 |
exit() | ✅ 会执行 atexit + 静态析构 |
std::terminate() | ❌ 不保证 |
_exit() | ❌ 完全不执行 |
| 被 Task Manager 杀进程 | ❌ 不执行 |
| 崩溃(Crash) | ❌ 不执行 |
而浏览器的现实是:
绝大部分异常退出路径都不会走析构链
三、那为什么还要 RAII?
这里是关键认知转折点:
RAII 是为了“运行时资源安全”,不是为了“进程结束清理”。
RAII 解决的是:
| 场景 | 是否需要析构 |
|---|---|
| 函数退出释放锁 | 必须 |
| 作用域结束释放文件 | 必须 |
| 线程结束释放资源 | 必须 |
| 进程结束 | 没必要 |
因为:
进程结束 = 整个宇宙被抹掉
四、Chromium 为什么“故意不析构”?
这是浏览器架构最核心的稳定性策略之一。
退出阶段是全局最不稳定状态
进程退出时:
-
线程在停止
-
IO 线程可能已经销毁
-
日志系统可能已关闭
-
UI 线程正在 shutdown
-
单例析构顺序不可控
如果这时还让对象析构,会发生什么?
| 情况 | 后果 |
|---|---|
| A 析构里访问 B 单例 | B 已析构 → 崩溃 |
| 析构里打日志 | 日志系统已死 |
| 析构里发 IPC | IPC 通道已关 |
| 析构里加锁 | 锁已销毁 |
所以 Chromium 的原则是:
🔥 退出阶段禁止复杂逻辑
五、真正的风险不是“泄漏”,而是“析构”
这是很多人认知颠倒的地方。
在浏览器内核中:
| 行为 | 危险程度 |
|---|---|
| 不 delete 内存 | ❌ 几乎无害 |
| 析构里访问全局资源 | 💀 极高风险 |
| 析构顺序不可控 | 💀💀 |
| 退出阶段跑复杂逻辑 | 💀💀💀 |
所以工程选择变成:
宁可泄漏,也不析构
六、为什么 OS 回收比析构安全?
因为 OS 的回收是:
✔ 原子级
✔ 无顺序依赖
✔ 无逻辑执行
✔ 不会崩溃
而析构是:
❌ 有顺序
❌ 有依赖
❌ 有逻辑
❌ 可能崩
七、Chromium 官方是怎么做的?
大量代码使用:
static base::NoDestructor<MyService> instance;
它的本质是:
// 构造,但永不析构
也有很多地方直接:
static Foo* g_foo = new Foo();
并明确标注:
// Intentionally leaked on shutdown.
这是一种工程策略,不是疏忽。
八、什么时候必须析构?
只有一种情况:
对象管理的是“进程级别之外的资源”
比如:
| 资源 | 是否必须主动释放 |
|---|---|
| 共享内存 | 是 |
| GPU context | 是 |
| 文件持久化缓冲 | 是 |
| 数据库事务 | 是 |
但 Chromium 也不会放在析构里,而是:
提供显式
Shutdown()阶段
九、析构函数在浏览器中应该做什么?
正确答案:
几乎什么都不做
析构只应该处理:
-
本对象的纯内存资源
-
不依赖其他模块的内容
十、总结:浏览器世界的生命周期哲学
| 普通 C++ | 浏览器内核 |
|---|---|
| 追求 0 leak | 追求 0 crash |
| 析构是必须 | 析构是风险 |
| 生命周期精细管理 | 退出直接放弃 |
| delete 是责任 | delete 是可选 |
一句话总结:
在浏览器进程里,“让 OS 收尸” 比 “优雅善终” 安全得多。
如果你现在在写浏览器、引擎、IDE、数据库、服务器这类常驻大进程系统,这套思维转变是必经之路。
412



被折叠的 条评论
为什么被折叠?



