当程序因为内存耗尽而抛出 std::bad_alloc
异常时,这并不意味着程序必须崩溃或停止运行。我们应该考虑“内存不足”作为一种可能正常出现的情况(“Out of memory? Business as usual.”),并设计应用程序能优雅地处理这种异常。
具体点说:
- 有些应用可能因为内存耗尽而进入死循环或卡住,表现为“不终止”。
- 这是因为程序没有合理地捕获和处理
std::bad_alloc
,导致无法恢复或清理资源。 - 更健壮的设计是在内存分配失败时能捕获异常,进行资源回收、释放内存,甚至降级服务或重启部分逻辑。
- 这对于内存受限或实时系统尤其重要。
换句话说,这段话强调:
遇到内存分配失败时,程序不必惊慌失措,而应该做好异常处理,确保即使在极端条件下依然保持可控和可恢复状态。
这是在讨论和澄清内存不足异常(std::bad_alloc
)相关的问题,具体分三点:
1. What is “out of memory” and what is “bad allocation”
- **“Out of memory”**指的是系统无法满足程序请求的内存分配请求,因为可用内存已耗尽。
- **
std::bad_alloc
**是C++标准库中在new
操作符内存分配失败时抛出的异常,用于通知程序分配内存失败。
简单来说,“out of memory”是一个系统状态,“bad allocation”是该状态在C++层面上的具体表现。
2. What do those who catch std::bad_alloc
do with it
- 捕获
std::bad_alloc
的程序员通常做什么?- 尝试释放资源或回收内存,减轻内存压力。
- 降级功能或通知用户,比如提示“内存不足,部分功能不可用”。
- 日志记录,以便后续分析。
- 优雅退出,确保程序不崩溃,保存用户数据。
- 有些系统可能重试分配或延迟操作。
3. How common (or rare) are these applications
- 捕获并处理
std::bad_alloc
的应用程序并不常见,特别是普通桌面或后台程序,很多程序默认遇到内存耗尽直接终止。 - 需要高度健壮性的应用,如金融系统、嵌入式系统、服务器软件或实时系统,更可能设计成捕获并处理这类异常。
- 但总体来说,处理内存不足异常的程序属于少数,且编写难度较大。
总结:
这三点帮助理解内存不足的本质、异常处理实践和现实中应用程序的普遍性。它鼓励我们设计更健壮的程序,主动捕获std::bad_alloc
,避免程序因内存耗尽而直接崩溃。
这段话讲的是资源(resource)和资源获取失败的情况,具体点解读如下:
1. 资源的定义和分类
- 资源 ≈ 有限的可用量的东西(根据Wikipedia和cppreference的定义)。
- 假定资源(Assumed resources):
- CPU时间、CPU核心、CPU缓存
- 网络带宽
- 随机数发生器的熵
- 电力供应
- 栈内存
这些通常被系统或平台保证或默认可用,程序不直接检测它们是否耗尽。
- 需检测的资源(Checked resources):
- 磁盘空间
- 硬件设备(文件描述符、套接字描述符)
- 线程、锁
- 其他软件资源
- 堆内存
这些资源申请可能会失败,程序需要主动处理。
2. 资源申请失败时的异常类型
std::system_error
:- 线程相关对象(如thread、unique_lock、shared_lock)
- 可能shared_ptr
- 网络套接字(basic_socket)
- 图形显示对象(display_surface)
- 这类异常代表系统调用失败或底层操作失败。
std::bad_alloc
:- 主要表示内存申请失败(堆分配失败)。
- 发生在很多标准库容器和资源分配中,如vector、list、map/set、string、function、shared_ptr等。
- 还包括一些更高级的组件如promise、packaged_task、正则表达式等。
总结:
- 资源不是单一的“内存”,而是多种有限系统资源。
- 资源申请失败时,C++标准库通过不同的异常类型来通知程序员,主要是
std::bad_alloc
和std::system_error
。 - 理解这点对编写健壮、可靠的程序尤其重要,必须捕获并合理处理这些异常。
这段是在解释操作系统中“内存耗尽(Out Of Memory, OOM)”的真实含义,尤其从虚拟内存和系统管理角度来讲。我们逐条分析:
1. “Memory” means page-based virtual memory
❝ “Memory” 指的是基于分页的虚拟内存。
- 在现代操作系统中,每个进程看到的是一个线性的虚拟地址空间(如从
0x00000000
到0xFFFFFFFF
),而这个地址空间被**按页(通常4KB)**划分。 - 每页有不同的属性:
- 私有 or 共享(private/shared)
- 只读 or 写时复制写入(COW, Copy-On-Write)
- 干净 or 脏页(clean/dirty)
- 常驻内存 or 已被换出(paged-out)
这意味着:你看到的“内存地址”并不代表真实物理内存位置,它是操作系统通过页表管理的抽象。
2. “Unused memory is wasted memory”
❝ 操作系统会用空闲内存做缓存或缓冲,如果程序不在用,那系统就拿去干别的。
- 所以现代操作系统(如 Linux、Windows)通常会尽量填满物理内存,用于:
- 文件缓存(page cache)
- IO buffer
- 共享库等
- 空闲内存越少≠出问题,反而可能是系统在高效工作。
3. Commit charge
❝ Commit charge 指的是所有可写但没有文件后备的页的总和,包括:
- 栈(stack)
- 数据段(data)
- 堆(heap)
- 私有
mmap
- 共享库的 .GOT 表(全局偏移表)等
这些内容不是来自文件,因此一旦写入,系统必须提供**物理内存或交换空间(swap)**来存它们。
4. When commit charge > (RAM + Swap), is it OOM?
❝ 当 commit charge 超过 可用RAM + 可用swap 时,是不是一定内存溢出(OOM)?
答案是:未必!
- 操作系统通常是**懒分配(lazy allocation)**的 ——
malloc
申请内存成功 ≠ 系统马上分配物理页。 - 真正使用(写入)时才分配页框(称为“触发页面错误”)。
- 所以,可能程序申请了一堆内存,但还没实际用 → commit charge 没增加多少。
- 一旦你写入太多页,系统发现“没有物理内存或swap能保证这些数据”时,才触发OOM。
总结
概念 | 说明 |
---|---|
虚拟内存 | 每个进程有自己的地址空间,分页管理 |
页面属性 | 可读/可写、共享/私有、驻留/换出 |
commit charge | 所有需要内存支持的、可写、非文件映射页的总量 |
OOM条件 | 当系统无法为潜在写入页提供内存或swap时,可能OOM |
懒分配机制 | 实际用内存(写入页)才会真正占用资源 |
这段是在讲 Linux 下 fork()
与大内存分配之间的经典问题,特别是在 fork/exec 模式下的大程序启动机制对内存的隐性影响。
fork/exec 问题解释
背景:
在 Unix/Linux 上,执行新程序通常分两步:
fork()
:父进程复制自己(内存等资源)→ 得到一个子进程exec()
:子进程替换自己,加载新程序(二进制)
然而,这个“复制”的动作有代价。
问题描述:
当你的进程分配了大量内存(比如 10GB 或 20GB),即使你只是想调用 exec()
启动一个新程序,在这之前你必须成功调用 fork()
。
❝ 但
fork()
会复制整个地址空间(虚拟地址页表) —— 这可能会因为内存不足而失败。
为什么小程序没问题?
- Linux 的
fork()
使用了 写时复制(COW):- 父子共享内存页,直到某一方尝试写入。
- 所以 fork() 通常 不立即复制全部内容,看起来是“便宜”的。
但!
为什么 fork()
会失败?
即使使用了 COW,系统仍然必须保证“如果将来子进程写入,它能支持分离写入”,这意味着:
fork() 成功 ⟺ 系统拥有足够的 commit space 来承诺:
“即使你们都写入每一页,我也能支持”。
于是:
malloc(20GB)
成功,但未写入fork()
失败:系统发现你请求的20GB,再加上子进程可能的20GB,系统撑不住 → 拒绝
对比 Windows:
Windows 没有 POSIX 的 fork()
模型,常用 CreateProcess()
:
- 一步到位加载新进程
- 不会复制当前进程的内存
- 避免了 Linux 的 fork/exec 模式下的 OOM 问题
替代方案:
方案 | 描述 | 问题 |
---|---|---|
vfork() | 暂时共享地址空间,不复制内存 | 子进程不能访问内存(容易出错) |
posix_spawn() | 内核级替代 fork/exec,效率高 | 语法复杂,不如 fork/exec 灵活 |
使用线程池 + 管道 | 父进程控制资源,避免复制 | 不适用于 exec |
总结核心点:
问题 | 解释 |
---|---|
fork() 复制整个虚拟内存空间 | 即使使用了 COW,系统也要保证“万一全写了”也能撑住 |
大量 malloc + fork 就可能触发 “Out of Memory” | 尤其是在总内存 + swap 不足时 |
exec() 会丢掉子进程的内存 | 但得等 fork() 成功后才能调用 exec() |
Windows 不用 fork,因此没有这个问题 | 它直接用 CreateProcess() |
如果你在构建一个 高内存使用后台服务,需要频繁调用子进程,可以考虑: |
- 避免用
fork()
,改用posix_spawn()
或 worker 模型 - 把子进程启动放在内存膨胀之前
理解 —— 这段在讨论不同操作系统如何处理**内存 overcommit(过度分配)**的问题,也即:操作系统是否允许你申请比物理内存(+swap)还多的内存,以及在不够时是否立刻失败。
什么是 Overcommit?
Overcommit:允许进程分配的虚拟内存总量超过系统实际可用物理内存 + swap。
原因是:
实际分配 ≠ 实际使用,大多数内存分配(如 malloc)最终未被访问。
两种策略的对比:
严格 commit accounting(不允许 overcommit)
- 系统在你分配内存时就检查是否“将来”能提供这块内存。
- 如果不能保证,将立即返回
std::bad_alloc
/ENOMEM
。 - OS:Windows, Solaris, HP-UX 等
- fork() 更容易失败:因为 fork() 理论上可能复制全部内存。
Overcommit + OOM killer(允许分配,必要时强制杀死进程)
- 系统允许几乎任意分配,但一旦物理内存耗尽,会触发 OOM 杀手。
- 被杀的进程是由内核算法决定的,不可预测。
- OS:Linux(默认使用 heuristic 模式)
Linux 的 overcommit 策略
在 Linux 中,控制 overcommit 的 sysctl 变量是:
/proc/sys/vm/overcommit_memory
可取值:
值 | 含义 |
---|---|
0 | 默认:启发式。可能 overcommit,内核根据启发式算法判断 |
1 | 始终允许 overcommit(不做检查,分配成功 ≠ 最终成功) |
2 | 严格限制:禁止 overcommit,分配必须有 backing store |
相关机制:
oom_adj
/oom_score_adj
:影响进程被 OOM 杀手选中的概率cgroups
:可用来对进程群组设定内存限额,配合 OOM 控制更有效rlimits
:即使在 overcommit 模式下,进程资源限制仍有效(如RLIMIT_AS
)
其他系统特色
系统 | 行为 | 说明 |
---|---|---|
AIX | 默认严格,但可通过 SIGDANGER opt-out | 信号通知进程“快没内存了” |
FreeBSD | 可系统关闭 overcommit,也可进程层面控制 | 使用 protect(1) |
Linux | 默认 heuristic,可调 | 兼顾灵活性与安全性 |
补充细节
- 在 Linux 上,即使启用了 overcommit,fork() 仍有可能失败,如果启用了 COW accounting(如 strict 模式
2
)。 - 在
"never"
模式下,Linux 会预留一部分内存专门给 shell/top/kill —— 以便用户可以杀掉 OOM 进程。这是防止系统彻底卡死的一个机制。 "always"
模式能带来性能,但也更容易导致非确定性崩溃。
总结重点
项目 | 内容 |
---|---|
“Overcommit” 是什么 | 虚拟内存分配可以超过物理内存 |
严格模式 | 更安全、更可预测,但 fork() 等操作容易失败 |
宽松模式 | 更灵活但不可控,可能触发 OOM 杀手 |
Linux 默认行为 | 启发式(heuristic),可配置 |
fork 问题 | 和 overcommit 策略强相关(特别是 fork + 大内存) |
这段内容揭示了围绕内存分配失败的一些**“常见误区(myths)”**,尤其是针对 Linux 系统上的 new
/malloc
行为。
核心观点:别太相信“分配总成功”的说法
Myth(误区):
“在 Linux 上,内存分配总是成功的。”
这是错误的泛化,很多人(包括流行库的作者)误认为:
new
或malloc
永远不会失败;- 或者,只有地址空间耗尽才会抛出
std::bad_alloc
。
举例
- Herb Sutter 曾批判这类想法(见他的《To new, perchance to throw part 2》)。
- LevelDB issue #335:也在讨论异常安全时提到:“Linux only throws
std::bad_alloc
when address space is exhausted”。
实际情况:Linux 并非总是 overcommit!
Linux 的默认设置是:
vm.overcommit_memory = 0
这是启发式(heuristic)模式,而不是 always-overcommit。
所以在以下情况中,你的分配 可以失败:
- 内核估算 commit charge 过高,拒绝了分配;
- 显式切到 strict 模式(
vm.overcommit_memory = 2
); - 使用 cgroup、rlimit 等限制;
mmap()
的匿名映射在某些配置下会检查 backing store;- fork() 由于写时复制(COW)导致潜在的高 commit,系统提前拒绝。
误信 myth 的后果
如果你写的是系统级代码、高可靠服务、或者内存敏感代码(如数据库、图形引擎、浏览器):
- 忽视
new
失败会导致非预期 crash; - 无法优雅处理 OOM 场景;
- 程序可能在压力下行为不稳定、不安全。
总结
Myth | Reality |
---|---|
Linux 上 new 永远成功 | 错误。Linux 的默认行为是启发式 overcommit,有可能拒绝 |
只有地址空间耗尽才抛 std::bad_alloc | 错误。commit charge 超过估算也可能触发 |
不用关心 new 是否失败 | 错误。在部分配置和工作负载下,失败是可能的而且严重的 |
如果你希望,我可以帮你: |
- 检查你的 Linux 当前的 overcommit 配置;
- 写一段 C++/C 代码来实验
malloc
或new
是否失败; - 设置或查询 overcommit 参数。
这段话阐述了一个重要安全概念:
“内存分配失败(bad allocation)不等于内存不足(OOM)”
核心观点
std::vector<int>(-1); // 这是坏分配 (bad allocation),不是 OOM
- 这个代码试图创建一个大小为
-1
的vector
(即size_t(-1)
), - 这相当于分配几乎
2^64 - 1
个元素 —— 明显是程序员逻辑错误,不是内存用光了才失败的。
为什么这很重要?
很多安全漏洞(甚至是 CVE)都源于:
- 未经验证的输入
- 导致了极端的内存分配
- 最终程序崩溃或被拒服(DoS)
真实世界的例子(OOM DoS via bad allocation)
CVE | 漏洞组件 | 问题描述 |
---|---|---|
CVE-2016-2109 | OpenSSL | 恶意短编码引起解码器尝试分配大量内存 |
CVE-2016-2463 | Android | 伪造媒体文件触发异常大分配 |
CVE-2016-6170 | ISC BIND | 伪造 DNS UPDATE 消息引发 OOM |
CVE-2015-7540 | samba AD-DC | 恶意网络包触发异常内存使用 |
CVE-2015-1819 | libxml | 精心构造 XML 导致 OOM |
CVE-2014-3506 | OpenSSL | 伪造 DTLS 握手消息引发过度分配 |
CVE-2013-7447 | cairo | 特制图像数据导致 cairo 分配过大缓冲区 |
正确的防御措施
- 永远不要相信外部输入(如
Content-Length
)。 - 对分配前的值进行 范围检查。
- 使用
std::vector::reserve()
或std::string::resize()
之前先确保大小合理。 - 若使用
new
,在分配前验证:if (size > MAX_SAFE_SIZE) throw std::runtime_error("Size too big");
总结重点
项 | 内容 |
---|---|
bad_alloc ≠ OOM | 有时是由无效输入导致的逻辑错误 |
不信任输入 | 任何外部数据都可能是恶意的 |
测试不足 | 逻辑漏洞往往在单元测试中未暴露 |
CVE 案例 | 多个成熟库都曾因此类问题中招 |
防护建议 | 输入验证 + 分配前检查大小上限 |
如果你正在开发处理外部数据的系统(如网络、图像、音频、XML、协议栈),请始终将输入视为不可信。这种“简单的长度验证”常常是安全的第一道防线。 |
这段内容深入探讨了 C 与 C++ 在处理内存分配失败(尤其是 malloc
失败)时的现实问题,尤其是在面对攻击者输入或内存耗尽的场景下。
核心问题:内存分配失败了怎么办?
在 C 中,如果 malloc(n_bytes)
失败,会返回 NULL
。
但现实是 —— 你通常:
不知道如何优雅地把这个错误往上传达(尤其是跨多层函数调用)。
C中的处理方式
以下是几种现实中看到的处理策略:
方式 | 示例 | 缺点 |
---|---|---|
返回错误码 | if (!ptr) return ERR_ALLOC_FAIL; | 错误传播代码繁琐,容易漏 |
longjmp /setjmp | 跳出多层函数 | 极度易错,破坏栈结构,调试困难 |
直接中止进程 | g_error("failed to allocate...") | 非库友好:调用方无法恢复 |
忽略失败 | 直接解引用 | 崩溃、漏洞、DoS |
示例代码中 GLib 的行为: |
mem = malloc(n_bytes);
if (mem)return mem;
g_error("failed to allocate ..."); // 中止程序
C++ 如何改进?
在 C++ 中:
try {_input_buffer.resize(_isize); // 内部可能 new[],如果失败会抛 bad_alloc
} catch (bad_alloc) {// 统一异常处理逻辑log_error(...);drop_connection();
}
C++ 优点:
- 标准库
new
默认在失败时抛出std::bad_alloc
- 你可以
catch
到它 - 允许你在靠近“业务逻辑”层做集中处理
- 如果你愿意,还可以
set_new_handler()
做自定义清理或回退
但即使在 C++中,也不是银弹
- 如果没有仔细
try-catch
,程序仍会终止 - STL 容器里隐含分配的地方多(如
vector::resize
,map::insert
) - 异常传播对性能敏感路径不利(如实时系统)
- 一些库(如
absl::btree
) 可能选择不抛异常
更好的设计方式(C/C++ 通用)
- 验证所有外部数据 —— 特别是分配大小前:
if (_isize > MAX_SIZE) return error();
- 封装内存分配 —— 便于统一错误处理:
void* safe_malloc(size_t sz) {void* p = malloc(sz);if (!p) throw bad_alloc();return p; }
- 区分可恢复 vs 致命错误 —— 不要总是
exit()
。
总结对比
比较点 | C | C++ |
---|---|---|
内存失败信号 | NULL | bad_alloc 异常 |
错误传播 | 手动返回码/longjmp | try/catch 或传递异常 |
默认行为 | 多数库直接 abort | 抛出异常可捕获 |
可恢复性 | 依赖设计 | 更易于结构化恢复 |
安全隐患 | 忽略返回值、崩溃 | 忽略异常、资源泄露 |
建议
- 不管是 C 还是 C++,永远不要直接相信输入可安全用于分配
- 在分配前进行上限检查
- 用 RAII、异常处理(在 C++),或者封装错误检查(在 C)来实现健壮行为
- 在大型项目中统一封装内存分配接口
这部分深入分析了 C++ 中的内存分配失败(std::bad_alloc
)的处理现状,并通过真实案例展示其影响力和实际处理缺失的问题。
一次“分配”摧毁整个世界:CVE-2009-1692
这个 CVE 展示的是一种 JavaScript 滥用内存 的方式,能导致:
浏览器/平台 | 行为 |
---|---|
IE, Firefox, Safari, Chrome, Opera | 内存激增后崩溃 |
Konqueror、Wii、PS3、iPhone | 整个设备挂死,需要硬重启 |
Chrome | 仅标签崩溃,稍好一些 |
原因:大部分 JavaScript 引擎未对异常内存使用做合理限制。例如: |
let s = "A";
while (true) s += s;
或者构造巨大的数组、字符串、对象,诱使引擎分配超过系统可承受的内存。
那么 std::bad_alloc 真有人处理吗?
真实搜索:Debian Code Search
对 catch (std::bad_alloc)
的显式捕获进行统计:
- 共 341 个包、3043 处代码。
- 抓出几类处理方式:
| 类型 | 占比 | 行为 |
| -------------------------------- | — | ----------------------------- |
| 忽略(“Somebody else’s problem”) | 46% | 没有做任何 meaningful 处理 |
| 转换为错误码 | 23% | 设置 error flag / 返回错误码 |
| 转为自定义异常 | 13% | 更高级错误管理(但仍依赖上层) |
| 转为其他语言 OOM 异常 | 5% | Python 的PyErr_NoMemory()
,等 |
| 原样 rethrow | 4% | 不处理,只传播 |
结论:大多数项目并没有有效处理bad_alloc
,即使是大型项目或库。
深层次启示
为什么这么少人处理?
- 罕见性:现代系统 overcommit,有 swap,
bad_alloc
变得少见。 - 难以测试:制造 OOM 非常困难,特别是在 CI 或开发机上。
- 恢复复杂:一旦分配失败,很多对象可能已经部分构造,状态不明。
- 设计缺陷:默认 C++ 构造过程不适合 rollback/恢复,RAII 有局限。
建议与启发
☑ 如果你关心健壮性或面对不受信任输入:
- 任何外部输入都必须验证分配大小上限
- try-catch
bad_alloc
是必要但不充分条件,你需要设计出清晰的 错误传播路径 - 封装资源敏感操作,比如:
std::optional<std::vector<char>> safe_allocate(size_t n) {try {return std::vector<char>(n);} catch (const std::bad_alloc&) {return std::nullopt;}
}
- 在大型系统中考虑更强的 OOM 策略,如:
- 内存池+监控机制
- 分配失败时释放低优先级资源
- 限制最大内存占用
总结
- 现实中一次失控的内存分配可以导致整个设备挂死(如 CVE-2009-1692)
- 即使在现代开源软件中,大多数
bad_alloc
是 未被优雅处理 的 - 你若做的是面向用户、网络、或嵌入设备的软件,必须认真对待 OOM
- C++ 提供了异常处理机制,但不意味着它自动解决了恢复问题
这一页继续分析了在 C++ 中如何应对 std::bad_alloc
,并结合真实代码仓库中的例子展示了多种常见的应对策略。
主题总结:C++ 库中如何“处理”分配失败
C++ 的内存分配失败默认抛出 std::bad_alloc
异常,但实际应用中,开发者的应对方式五花八门,本页主要展示了两类更“实际”的做法:
① “Convert to Error Code”(转换为错误码)——占 23%
这类库通常采用如下策略:
- 在内部分配失败时 catch 异常
- 然后返回某种错误码(而不是让异常继续传播)
示例:Notepad++ / Scintilla
try {if (_pscratchTilla->execute(SCI_GETSTATUS) != SC_STATUS_OK)throw;InsertString(position, data, length);
} catch (std::bad_alloc &) {return SC_STATUS_BADALLOC;
}
优点:调用者可以统一地检查错误码并处理,不需要处理异常
缺点:你得永远记得去检查返回值,否则就白做了
来源代码:
- Scintilla Buffer.cpp
- Scintilla Document.cxx
② 置空指针 + 状态标志(member variable update)
一些更底层的库(如 libstdc++)不会直接抛异常,而是在失败时设置标志位或返回空指针。
示例:libstdc++ / GCC 的 ios
stream 处理
try {_words = new (std::nothrow) _Words[_newsize];
} catch (const std::bad_alloc&) {_words = nullptr;
}
if (!_words)_M_streambuf_state |= badbit;
std::nothrow
会阻止抛出异常,失败时返回 nullptr
状态标志 badbit
会告诉流使用者出了问题,但不致 crash
来源代码:
- GCC libstdc++ ios.cc
小结:这种做法的“哲学”是?
稳定优先:比起抛出异常破坏流程,不如用返回码/状态位尽量“稳住”
可恢复性强:尤其在嵌入式或交互式应用中,更倾向于用错误码回传错误
对比 catch 异常的做法:
方法 | 表现 | 优点 | 缺点 |
---|---|---|---|
抛出 bad_alloc | 直接跳出到上层 | 自动传播 | 稳定性差,易崩 |
返回错误码 | 调用者自行判断 | 更稳健 | 容易忘记检查 |
设置状态位 | 延迟处理 | 不会中断流程 | 易被忽视 |
实战建议:
- 若你写的是“控制逻辑类库”,建议用 返回错误码
- 若你写的是“算法或组件库”,可以保留
bad_alloc
异常,让上层处理 - 若你是做 UI、嵌入式、或系统层调用,尽量别崩,catch
bad_alloc
并恢复 是最佳实践
本页内容要点:
继续讲 C++ 实战中对 std::bad_alloc
的应对方式,尤其聚焦于:
③ Convert to custom exception(转换为自定义异常)——占 13%
示例:POCO 项目中的 PostgreSQL 模块
代码片段摘自 Poco::Data::PostgreSQL::Binder
:
try {if (aPosition >= _bindVector.size())_bindVector.resize(aPosition + 1);InputParameter inputParameter(aFieldType, aBufferPtr, aLength);_bindVector[aPosition] = inputParameter;
} catch (std::bad_alloc&) {throw PostgreSQLException("Memory allocation error while binding");
}
解释与动机:
这段代码的作用是:在 PostgreSQL 参数绑定时,如果分配内存失败,抛出特定的数据库异常。
为什么不直接让 bad_alloc
传播?
std::bad_alloc
语义太笼统:只表示“内存不足”,调用者难以分辨哪里出错- 业务逻辑希望知道:“这是数据库层的绑定问题”,而不是其他未知的系统异常
- 抛出更语义明确的异常
PostgreSQLException
,便于上层精确捕获和处理
通用做法:
这种模式适用于 中间件、数据库接口、网络通信库 等系统:
“底层异常 + 语义变换 = 业务语境中的异常”
具体实现步骤通常是:
- 捕获如
std::bad_alloc
这种通用异常 - 转换成你的领域自定义异常(加上有意义的信息)
- 重新抛出给上层
自定义异常的优点:
优点 | 说明 |
---|---|
语义清晰 | 可以根据上下文区分哪一层出了问题 |
更好调试 | 带有错误文本,便于记录日志或提示用户 |
更好管理 | 可将所有异常统一包裹成某种“领域异常” |
注意事项:
- 如果你抛出自定义异常,务必让其继承自
std::exception
,否则catch (std::exception&)
捕不到 - 如果你使用异常做控制流,记得别在性能关键路径上滥用
总结:
这一类处理方式体现了 “异常语义升维”:把底层的问题提升成高层可以理解和应对的业务语境。
如果你在写自己的 C++ 框架,建议在模块边界用这个策略,例如:
catch (const std::bad_alloc&) {throw MyApp::DatabaseError("OOM when preparing query buffer");
}
本页内容:std::bad_alloc
的第四种处理方式
④ Rethrow as-is(原样重新抛出)——占 4%
示例代码片段:
m_allocations.push_back(ptr);
catch (const std::bad_alloc& e)throw e; // 显式重新抛出异常
delete[] ptr;
catch (...) throw; // 捕获所有异常并重新抛出
_allocator.deallocate(_instance, 1);
_instance = 0;
throw;
含义解释:
- “Rethrow as-is” 指的是:捕获异常只是为了清理资源,清理完成后立即重新抛出原始异常。
- 异常不转换、不吞掉、不降级为 error code。
换句话说:
我只打扫战场,不阻止你继续报警。
为什么这么做?
在 C++ 中使用 RAII(资源获取即初始化)可以自动释放大多数资源。但 某些资源不是 RAII 管理的,比如:
- 显式堆分配但没有用智能指针管理的资源
- 显卡资源、文件描述符、数据库连接等外部资源
- 全局状态(比如
_instance
)
这种情况下,如果直接抛异常,你可能会资源泄漏或 状态混乱。
实用模式:
try {ptr = new int[1024];m_allocations.push_back(ptr);
} catch (const std::bad_alloc& e) {delete[] ptr; // 手动清理throw; // 原样抛出
}
或在 Singleton 模式中:
try {_instance = _allocator.allocate(1);
} catch (const std::bad_alloc&) {_instance = nullptr;throw; // 让调用者知道失败原因
}
注意事项:
风险点 | 描述 |
---|---|
throw e; | 这样会 slicing(异常对象复制)且重置 what() 栈信息。应使用 throw; 保持异常原样 |
忘记清理 | 仅适用于你确实在 catch 中完成了非 RAII 资源的清理 |
复杂代码中 | 若 catch block 很长,重新抛出前可能会混淆上下文逻辑,要留意可维护性 |
总结:
Rethrow-as-is 是一种 严谨负责的清理行为,适用于“我做了一点事,然后继续让上层来决定”。
优点:
- 不吞异常、不篡改语义
- 能清理局部非 RAII 资源
缺点: - 不能完全避免资源泄漏(建议配合智能指针)
- 不能添加业务语义(不像前面讲的“Convert to custom exception”)
本页重点:
“Cleanup and terminate” —— 占总样本 21%
主要分类:
类型 | 比例 | 说明 | 代表项目 |
---|---|---|---|
Not from main | 12% | 在非主线程(非 main 函数)捕获异常后清理并终止 | rethinkdb, ipopt, fluxbox, krita 等 |
From main | 8% | 在主线程的 main 函数里捕获异常后清理并终止 | tripwire, smartmontools, tango 等 |
Configurable handler, default abort | 1% | 配置异常处理器,默认调用 abort | igraph, gdal 等 |
“Parachute” | <1% | 代码中带有“安全降落伞”风格的异常处理(参考《Code Complete》) | scantailor |
意思是什么?
- 捕获异常后,程序不能安全继续运行,就进行清理资源并优雅退出。
- 不是试图“恢复”或转换异常,而是“最后保护”,保证资源正确释放,输出错误信息,然后终止程序。
- “Not from main”通常是子线程或库内部异常,这种情况下清理后终止避免不确定状态。
- “From main”意味着在程序入口集中处理所有异常。
“Parachute” 异常处理:
- 来自 Steve McConnell《Code Complete》概念
- 程序中加入异常“安全网”,防止任何异常逃逸到操作系统导致崩溃。
- 保障程序的最小安全退出,保证日志和资源回收。
总结:
这种策略通常适用于:
- 对内存安全和稳定性要求极高的系统(服务器、嵌入式、图形驱动)
- 不能容忍异常破坏状态的程序
- 只希望在异常时尽可能干净退出
如果你要设计 Vulkan 框架底层,也应该考虑在顶层捕获异常,做最后清理和终止,避免崩溃时资源泄漏或硬件状态异常。
需要我帮你写一个 C++ 程序主入口的异常安全模板吗?包括统一异常捕获,日志记录,资源释放,安全退出。
核心要点补充:
- “Cleanup” 通常是指:
- 删除临时文件、锁文件,释放有限资源,避免“僵尸”或资源泄漏。
- 例如
unlink_ofile(oname);
删除临时输出文件。
- 异常捕获后立即清理并退出:
- 例子中捕获
std::bad_alloc
,打印错误信息,退出程序。
- 例子中捕获
- 库内部直接崩溃(
CRASH()
)示例:- 在
fastMalloc
里,如果malloc
返回空指针,直接调用崩溃函数。 - 这种做法代价高,但防止异常传播导致状态不确定。
- 在
设计启示
- 在 Vulkan 或高性能计算框架中,底层内存分配失败可考虑:
- 统一捕获 bad_alloc,安全清理后终止,防止状态崩溃。
- 对临时资源(缓冲区、锁文件等)要有明确的释放策略。
- 对于无法恢复的分配失败,宁可安全崩溃,避免隐蔽错误。
这部分意思是:
面对 std::bad_alloc
,一些程序选择 “继续向前走”,采取不同的策略,而不是直接终止:
- 尝试分配更少的内存(3%)
- 例如用更小的缓冲区,或改为即时计算,减少内存需求。
- 例子:Audacity、Eigen3、LibreOffice 等。
- 在析构函数中吞掉
bad_alloc
(3%)- 为了缓存或延迟操作,在析构时悄悄处理内存不足。
- 例子:LibreOffice、OpenCV 等。
- 使用替代算法(2%)
- 比如改用“就地”算法避免额外内存分配。
- 例子:VTK、Krita、Octave 等。
- 释放内存(清理缓存、腾出空闲链表)(2%)
- 尝试通过释放缓存或内部资源来恢复内存。
- 例子:libstdc++、Sonic Visualizer。
- 重试分配(1%)
- 直接尝试重新申请内存,期望短期内资源释放后成功。
- 例子:GNU Radio。
设计启示
- Vulkan框架设计时,可以提供多级降级策略,面对OOM:
- 优先减小数据量(如分块计算)
- 使用更节省内存的算法路径
- 动态清理缓存
- 甚至尝试重试
- 只有最后才彻底失败或终止
- 这样能增强系统健壮性和灵活性,提升用户体验。
这段代码示范了遇到内存分配失败(std::bad_alloc
)时,优雅切换到“就地”算法(in-place algorithm),以节省内存,保证程序继续运行。
代码逻辑解析:
bool inline inplace_transpose(arma::Mat<eT>& X)
{try {X = arma::trans(X); // 先尝试直接转置(需要额外内存)return false; // 成功,返回false表示没有用“就地”方法}catch (std::bad_alloc&) {
#if (ARMA_VERSION_MAJOR >= 4) || ((ARMA_VERSION_MAJOR == 3) && (ARMA_VERSION_MINOR >= 930))arma::inplace_trans(X, "lowmem"); // 失败后调用就地转置,节省内存return true; // 表示用了“就地”转置
#endif}
}
- 先尝试普通转置,失败时抛出
bad_alloc
- 捕获异常后,切换到就地转置,避免开辟新的内存空间
- 返回值表示是否用就地方法
启示:
- 设计高性能或内存敏感框架时,预设“备选算法”非常重要。
- 在OOM时,尽量避免直接崩溃,切换到内存占用更小的“降级方案”。
- 对外接口上,告知调用方是否使用了备选算法,有利于调优或告警。
这部分讲的是面对 std::bad_alloc
(内存分配失败),软件通常采取“回滚并做别的事情”的策略,避免程序崩溃,提升用户体验和系统稳定性。具体做法分成几类:
主要做法:
- 交互式应用拒绝用户操作(约12%)
- 比如打开文件失败,弹错误框,提醒用户。
- 例子:Notepad++, LibreOffice, Inkscape, TeXstudio 等。
- 服务器拒绝服务请求(约5%)
- 服务器因资源不足,直接丢弃新请求,保证现有服务稳定。
- 包括网络服务器(ntopng, apt-cacher-ng等)和数据库(scylladb, tarantool等)。
- 预设降级或替代方案(约1%)
- 使用默认资源代替失败的加载,比如错误纹理、无声驱动等。
- 例子:游戏0ad、模拟器desmume。
设计启发
- 在 Vulkan 或其它底层框架设计中,应允许调用者检测内存失败并优雅“退回”,比如拒绝提交任务、通知用户、加载备用资源。
- 设计接口时,支持错误返回或异常捕获,允许“放弃当前操作但继续运行”。
- 对于服务器或批处理任务,提供负载调节或请求拒绝机制,避免崩溃。
这段内容细化了“回滚并做别的事情”里交互式应用的典型处理:
- 最常见场景是文件太大,无法加载,比如
Poppler::Document::load(fileName)
失败时抛std::bad_alloc
,然后返回错误状态(PopplerErrorBadAlloc
),并返回一个空的智能指针,避免程序崩溃。 - 另一个例子是
vtkstd::bad_alloc
抛出时,用自定义异常IRISException
包装,再通过 UI 弹窗警告用户“生成网格时内存不足”,这样用户能感知错误且程序保持稳定。
这体现了: - 优雅地捕获内存分配失败异常,避免直接崩溃
- 返回错误状态或抛出自定义异常
- 及时向用户反馈错误信息
- 保证程序其余部分继续稳定运行
这里讲的是服务器端遇到 std::bad_alloc
时的典型处理:
- 服务器通常会捕获异常后放弃当前请求,避免整个服务崩溃或卡死。
- 具体例子里,
processPacket(...)
可能抛出bad_alloc
,catch 捕获后打印日志(或其他轻量级处理),然后直接跳过这条请求,继续处理后续数据包。
这体现了服务器稳定性和容错性的设计思路: - 服务器优先保证持续服务能力
- 某些请求内存不足导致失败时,宁愿放弃该请求,也不影响整体系统正常运行
- 记录日志帮助排查和监控
这部分讲的是单元测试中对 std::bad_alloc
异常的捕获和验证:
- 单元测试(Unit Tests)会故意触发
bad_alloc
,检查代码在内存分配失败时的表现是否符合预期。 - 通过宏或断言(如
ASSERT_THROWS_IN_TEST
)确认特定操作会抛出std::bad_alloc
。 - 这样可以确保程序在异常情况下依然稳定或做出正确反应。
举例来自 TBB 的测试代码,模拟内存压力,确认容器操作抛出异常,保证健壮性。
这一段给出了几个领域(办公软件、代码编辑器、浏览器、数据库)在遇到C++内存耗尽(OOM)时的典型表现和应对状况总结,体现了现实世界中std::bad_alloc
处理的复杂性和挑战:
- 办公软件中,很多程序在大文件或大粘贴操作时崩溃或异常退出,有些能存活但表现不稳定,极少数能优雅处理。
- 代码编辑器/IDE也类似,有的优雅拒绝操作,有的直接崩溃。
- 浏览器表现尤为复杂,JavaScript运行时OOM导致标签页崩溃,或者浏览器崩溃,部分能阻止脚本但仍会逐渐崩溃。
- 数据库有些能存活(ScyllaDB、Tarantool),大多数会崩溃,且社区呼声强烈希望数据库能更优雅地处理OOM,如取消导致OOM的查询并释放内存,而不是直接崩溃服务。
最后提到OOM处理的核心要点: - 用户确实需要OOM保护,尤其是避免数据丢失或服务中断时
- 一致的RAII风格编程是基础,避免析构时抛出异常
- 多种策略并存,极端情况可预分配全部资源
- 库的角色极其重要,有的库会使用户陷入困境,有的库(如Scintilla)则帮用户优雅处理OOM
整体来说,这是一段现实案例的总结和建议,告诉我们OOM是非常棘手的问题,必须从设计、代码、库、架构多方面入手,结合实际场景选择合适策略。