编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

    • README
    • 公共方法论

    • 体系建设篇

    • 资源专项篇

      • CPU监控与分析
      • 内存监控与治理
      • OOM与低内存治理
        • 01.阅读说明
        • 02.贯穿案例
          • 2.1 案例背景
          • 2.2 经验派的 3 周折腾(典型反面教材)
          • 2.3 方法派的 10 天闭环
          • 2.4 上线效果
          • 2.5 案例如何串起本文
        • 03.OOM 物理本质
          • 3.1 一句话定义
          • 3.2 现象与代价
          • 3.3 度量准则与基准
          • 3.4 反直觉问题清单
        • 04.触发条件原理
          • 4.1 OOM 四类触发条件
          • 4.2 跨平台同构原理
          • 4.3 跨平台术语对照
          • 4.4 平台差异点矩阵
        • 05.度量与采集
          • 5.1 三类采集方案
          • 5.2 各方案的可见盲区
          • 5.3 跨平台采集对照表
          • 5.4 数据可信度评估
        • 06.归因决策树
          • 6.1 OOM 归因决策树
          • 6.2 现场快照分析法
          • 6.3 OOM 前兆识别法
          • 6.4 多维度归因法
        • 07.堆 OOM 全链路
          • 7.1 Android Java 堆 OOM 全链路
          • 7.2 iOS 堆 OOM 全链路
          • 7.3 Web JS 堆 OOM 全链路
          • 7.4 Compose / SwiftUI 堆 OOM 全链路
          • 7.5 堆 OOM 全链路性能数据
        • 08.LMK/Jetsam 全链路
          • 8.1 Android LMK 全链路
          • 8.2 iOS Jetsam 全链路
          • 8.3 onTrimMemory 五级详解
          • 8.4 Web Tab Killer 全链路
          • 8.5 跨平台 LMK 对照
        • 09.地址空间全链路
          • 9.1 32 位虚拟地址空间布局
          • 9.2 地址空间碎片化原因
          • 9.3 64 位地址空间
          • 9.4 32 位时代的临时缓解
          • 9.5 跨平台地址空间对照
        • 10.资源耗尽全链路
          • 10.1 文件描述符耗尽
          • 10.2 线程数耗尽
          • 10.3 graphics 内存(GPU 内存)
          • 10.4 Web 资源耗尽
          • 10.5 跨平台资源对照
        • 11.跨端 OOM 全链路
          • 11.1 Compose / SwiftUI OOM 全链路
          • 11.2 Flutter OOM 全链路
          • 11.3 React Native OOM 全链路
          • 11.4 跨端 Hybrid(WebView)OOM 全链路
          • 11.5 跨端 OOM 对照
        • 12.跨端对照
          • 12.1 五个全链路总览
          • 12.2 各平台 OOM 优化优先级
          • 12.3 反直觉问题答疑
        • 13.治理一层堆
          • 13.1 缓存上限严控
          • 13.2 大对象分块化
          • 13.3 Bitmap 降采样 + 复用
          • 13.4 内存泄漏排查
          • 13.5 配置与监控
        • 14.治理二层 LMK
          • 14.1 onTrimMemory 5 级响应
          • 14.2 iOS didReceiveMemoryWarning
          • 14.3 提升前台优先级
          • 14.4 后台主动减负
          • 14.5 分级降级策略表
        • 15.治理三层地址空间
          • 15.1 升级 64 位(核武器)
          • 15.2 大对象用 mmap
          • 15.3 32 位时代的临时缓解
          • 15.4 监控地址空间
          • 15.5 跨平台地址空间治理对照
        • 16.治理四层兜底
          • 16.1 主动重启机制
          • 16.2 状态序列化与恢复
          • 16.3 重启过渡 UI
          • 16.4 ROI 排序
          • 16.5 避免反向收益
        • 17.求证实验 ⭐
          • 17.1 实验一:地址空间碎片
          • 17.2 实验二:低内存信号时机
          • 17.3 实验三:降级策略收益
          • 17.4 实验四:64 位升级的 OOM 收益
          • 17.5 实验五:兜底重启的用户体验
          • 17.6 五大实验启示
        • 18.实战案例
          • 18.1 跨端同构案例:分级降级 + 缓存上限
          • 18.2 平台特异案例:Android 7 32 位地址空间 OOM
          • 18.3 反例案例:catch OOMError 导致后续雪崩
        • 19.防劣化体系
          • 19.1 三道防线总览
          • 19.2 编码期 Lint
          • 19.3 CI 卡口
          • 19.4 线上 SLO
          • 19.5 文化建设
        • 20.跨平台速查
          • 20.1 工具速查
          • 20.2 关键 API 速查
          • 20.3 各平台 OOM 优化清单
        • 21.总结与延伸
          • 21.1 五条核心原则
          • 21.2 五个常见误区
          • 21.3 一句话总结
          • 21.4 延伸阅读
      • 线程模型调度优化
      • 进程与多进程优化
      • IO与存储性能
    • 流水线专项

    • 业务专项篇

    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 资源专项篇
杨充
2026-05-27
目录

OOM与低内存治理

# OOM 异常与低内存治理

本文核心命题:OOM 是内存治理失败的兜底信号,是"应用想要的"和"系统能给的"之间不可调和的冲突。治理 OOM 不是事后救火,而是事前建立"内存上限感"与"低内存降级"机制。OOM 治理 = 监控前兆 × 降级链路 × 兜底恢复。


# 01.阅读说明

  • 本文卷归属:卷二 · 资源篇 · 第 3 篇
  • 本文目标层级:L2 进阶 → L3 专家 → L4 架构
  • 适用平台:Android(主) / iOS / Web / 跨端框架 / 嵌入式
  • 前置阅读:
    • 卷二·02 内存监控与治理(OOM 是内存治理失败的兜底信号)
    • 卷零·06 性能预算与防劣化体系
  • 本文核心命题:

    OOM 是内存治理失败的兜底信号,是"应用想要的"和"系统能给的"之间不可调和的冲突。 治理 OOM 不是事后救火,而是事前建立"内存上限感"与"低内存降级"机制。

全文 21 章地图:

   §01 阅读说明           §02 贯穿案例           §03 OOM 物理本质      §04 触发条件原理
   §05 度量与采集         §06 归因决策树
   §07 堆 OOM 全链路 ⭐    §08 LMK/Jetsam 全链路 ⭐  §09 地址空间全链路 ⭐
   §10 资源耗尽全链路 ⭐   §11 跨端 OOM 全链路 ⭐   §12 跨端对照
   §13 治理一层堆 ⭐       §14 治理二层 LMK ⭐      §15 治理三层地址 ⭐    §16 治理四层兜底 ⭐
   §17 求证实验 ⭐         §18 实战案例           §19 防劣化体系          §20 跨平台速查
   §21 总结与延伸
1
2
3
4
5
6
7

阅读建议:先读 §02 案例 → §03/§04 拿到原理 → §05/§06 学会度量归因 → §07-§11 五大全链路(堆/LMK/地址/资源/跨端)→ §13-§16 四层治理 → §17 求证 → §18-§20 工程闭环。


# 02.贯穿案例

本案例贯穿全文:§03 看懂代价、§04 拿到触发条件模型、§05/§06 用三方案+决策树定位、§07-§11 五个全链路对应每一类 OOM、§17 用实验复盘、§13-§16 给出分层策略闭环。

# 2.1 案例背景

某短视频 App V5.2 上线"沉浸式 Feed + 自动播放",OOM 率激增:

  • 业务场景:主 Feed 滚动 + 自动播放视频;用户连续刷 50+ 条后崩溃。
  • 用户反馈:低端机 OOM 崩溃率 6.8%,旗舰机 1.5%;32 位用户问题尤其严重("内存看起来够用但还是崩")。
  • 业务损失:滚动深度(关键指标)远低于行业;投诉占崩溃类前三。

# 2.2 经验派的 3 周折腾(典型反面教材)

假设 措施 结果
视频缓存太大 减小 PreloadBuffer 50% OOM -10%
largeHeap 不够 manifest 设 largeHeap=true 无明显改善
视频解码内存高 启用硬解 中端机 OK,低端机仍崩

3 周累积,OOM 率仅从 6.8% 降到 5.8%。原因:真正的根因是 32 位地址空间碎片 + 低内存信号未响应——不是单纯的"分配过多"。

# 2.3 方法派的 10 天闭环

新接手的同学按本文方法论重做:

Day 1(§04 触发条件模型):分析 OOM 现场,发现 3 类共存:

  • A 类堆 OOM:占 25%(Java 堆达上限)
  • B 类地址空间 OOM:占 55%(32 位连续大块申请失败)
  • C 类 LMK:占 20%(系统压力大杀进程)

Day 2-3(§13 一层堆 + §14 二层 LMK):

  • 缓存上限严控(图片/视频缓存按 maxMemory/8 算)
  • onTrimMemory 4 级响应(差异化释放)

Day 4-7(§15 三层地址):

  • 双 abi App Bundle 上线 64 位
  • 大对象改 mmap

Day 8(持续优化):

  • Bitmap 下采样 + inBitmap 复用

Day 9-10(§16 四层兜底):

  • 主动重启机制(检测危险阈值即重启)

# 2.4 上线效果

指标 经验派 3 周后 方法派 10 天后
OOM 率(低端 32 位) 5.8% 0.4%
OOM 率(高端 64 位) 1.4% 0.05%
后台被杀率 22% 6%
单次浏览深度 12 条 38 条
用户感知崩溃比 100% 12%(主动重启效果)
广告变现 基线 +38%

核心反差:3 周经验派改了缓存大小 + largeHeap + 硬解视频——这些动作错过了 B 类(地址空间)和 C 类(LMK)。方法派靠"OOM 三类分类"识别真因,10 天降到 0.3%。

# 2.5 案例如何串起本文

  • §03/§04 物理本质 + 触发条件 ▶▶ 重定义"OOM"——区分堆 / 地址空间 / LMK / 资源耗尽四类。
  • §05/§06 度量+归因 ▶▶ 接入 KOOM/MetricKit 抓现场快照,决策树定位走"32 位地址空间碎片化"分支。
  • §07-§11 五大全链路 ▶▶ 堆/LMK/地址/资源/跨端 五条链路对应案例每一类 OOM。
  • §17 求证实验 ▶▶ §17.1 地址碎片、§17.2 信号时机、§17.3 降级、§17.4 64 位、§17.5 主动重启。
  • §13-§16 治理 ▶▶ "堆/LMK/地址/兜底"四层正是案例落地路径。

探索性思考:为什么经验派会反复猜错?因为他们把 OOM 当成单一问题。真实世界的 OOM 是一个"伪概念"——它包含至少 4 种完全不同的物理触发:堆耗尽 / 系统杀进程 / 地址空间碎片 / 资源耗尽。好的工程师面对模糊概念的第一动作是"分类" —— 不分类就无法精准施治。


# 03.OOM 物理本质

# 3.1 一句话定义

OOM = 进程"请求一次内存分配"时,系统无法满足,导致进程被终止或异常抛出。

这句话隐含三个不可商量的物理约束:

约束一:分配请求 vs 系统能力 是"瞬时供需"问题

OOM 不是"内存慢慢用满",而是某一次具体的 alloc 请求失败。即使应用平均占用很低,只要某一次需要大块连续内存(如 100MB Bitmap),系统拿不出来就 OOM。

约束二:物理内存 ≠ 可用内存

   物理 RAM (例如 6GB)
        ├── 内核占用      (~500MB,不可用)
        ├── 其他进程占用  (~3GB)
        ├── 文件缓存      (可回收,但需要时间)
        └── 真正"立即可用" (~1GB)
                            ↑
                  应用申请超过这部分会触发 OOM
1
2
3
4
5
6
7

约束三:地址空间限制是物理硬约束(特别是 32 位)

每个进程的虚拟地址空间有上限。32 位上限 4GB(用户态约 3GB),即使物理内存充足,单进程也无法用超过地址空间。32 位应用 OOM 经常发生在 PSS 还有空间但找不到连续虚拟地址。

# 3.2 现象与代价

OOM 是用户感知最严重的性能问题之一:

  • 应用直接崩溃:用户看到"应用已停止",是最差体验。
  • 后台被静默杀死:iOS jetsam / Android LMK 静默杀掉进程,用户切回时"重启"。
  • 首页加载失败:低内存设备进入首页时大对象分配失败,白屏 / 报错。
  • 特定场景必崩:拍照、播放视频等高内存场景在低端机上稳定复现。

业务代价(行业实测数据):

  • 头部 App:OOM 率每降 0.1% → 留存 +0.3% / DAU +0.5%。
  • 中低端机型(< 4GB RAM)OOM 率往往是高端机的 5-10 倍。
  • iOS 系统 jetsam 后用户无任何提示,体验类崩溃。

# 3.3 度量准则与基准

资源视角(USE):

指标 含义 阈值参考
内存使用率 进程内存 / 系统可用 < 70% 安全
内存饱和度 是否触发 swap / kill swap > 0 即告警
OOM 错误数 OOM 触发频次 每千次启动 < 1

请求视角(RED):

指标 含义 阈值参考
OOM 崩溃率 OOM / 总崩溃 < 0.1%
后台被杀率 不正常退出 / 启动总数 < 5%
内存压力等级事件 onTrimMemory 触发次数 应作为前兆

关键约定:OOM 不应只统计"崩溃",必须统计"前兆事件"(如 onTrimMemory、低内存警告)。前兆比崩溃早 10–60 秒,是治理的最佳窗口。

行业基准:

平台 OOM 率 后台被杀率 触发阈值
Android(4GB+) < 0.05% < 5% maxHeap = 256/512MB
Android(< 4GB) < 0.2% < 10% maxHeap = 128/256MB
iOS < 0.05%(jetsam) < 3% 由系统动态决定
Web < 0.01% N/A 浏览器 ~4GB / Tab
嵌入式 0%(必须) 0% 厂商配置

# 3.4 反直觉问题清单

带着这些问题阅读:

  1. 物理内存还够,为什么 32 位应用还是 OOM?
  2. 收到 onTrimMemory 一定要释放内存吗?释放多少?
  3. iOS 没有 OOM,jetsam 和 OOM 一样吗?
  4. Bitmap OOM 一定是 Bitmap 占内存太大吗?
  5. 后台被杀就一定是因为内存吗?
  6. 大 Bitmap 拆成小 Bitmap 还会 OOM 吗?
  7. 增大 largeHeap 一定能避免 OOM 吗?
  8. low-end 设备和高端设备的 OOM 阈值差多少?

探索性思考:为什么"OOM"作为概念在工程界被滥用?因为它字面直观——"内存不够了"。但实际上 OOM 包含至少 4 种独立的物理机制:堆/LMK/地址空间/资源。好的术语必须精确——把 4 种问题混成 1 个名词,是工程沟通最大的浪费。真正的高手治理 OOM 第一步:把"OOM"这三个字拆开。


# 04.触发条件原理

# 4.1 OOM 四类触发条件

OOM 不是单一原因,而是多个条件同时满足才触发。把这些条件画成树:

   OOM 触发 = 任一路径成立

   ┌────────────────────────────────────────────────────┐
   │ A. 进程级上限(应用 Heap 限制)                       │
   │    Android: maxHeapSize(128/256/512MB)            │
   │    iOS: jetsam 限制(动态,前台优先)                  │
   │    触发:Java OutOfMemoryError                        │
   ├────────────────────────────────────────────────────┤
   │ B. 系统级压力(设备整体低内存)                       │
   │    所有进程总和接近物理 RAM                            │
   │    触发:LMK 杀进程 / iOS jetsam                      │
   ├────────────────────────────────────────────────────┤
   │ C. 地址空间碎片(32 位特有)                          │
   │    虚拟地址池无法找到连续大块                          │
   │    触发:mmap / malloc 大块失败                       │
   ├────────────────────────────────────────────────────┤
   │ D. FD / Thread 等其他资源耗尽                         │
   │    file descriptors / threads 上限                   │
   │    触发:OOM-like 错误("Too many threads")           │
   └────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

关键认知:

  • A 类是应用层 OOM(Java 抛 OOMError)
  • B 类是系统层 OOM(进程被杀,应用层看不到)
  • C 类是 32 位特有(即使物理够,地址空间不够)
  • D 类是被错认为 OOM 的"资源耗尽"

这就是为什么 OOM 治理必须分类:四类的归因路径和优化手段完全不同。

# 4.2 跨平台同构原理

不同平台的具体表现差异巨大,但抽象成"分配请求超过供给上限"后完全同构:

   通用 OOM 模型:

      [应用申请 X 字节] ──▶ [系统检查能否满足]
                                    │
                              ┌─────┴─────┐
                              ▼           ▼
                          能 → 给       不能 → OOM
                                          │
                          ┌───────────────┼───────────────┐
                          ▼               ▼               ▼
                      应用层抛错        系统杀进程       静默失败
                      (Java OOM)       (LMK/jetsam)    (malloc NULL)
1
2
3
4
5
6
7
8
9
10
11
12

每个平台都必须解决:

抽象问题 解决什么问题
上限设定 每个进程能用多少(防一个应用拖垮系统)
压力感知 何时通知应用"快不够了"
决策机制 不够时杀谁、杀几个
应用响应 应用收到压力信号后如何降级

正因为它们都长这个样子,OOM 的本质也是同一个:

OOM 不是平台 bug,是"应用需求与系统供给"之间不可调和的冲突。治理就是建立"压力感知 + 主动降级 + 兜底恢复"三层机制。

# 4.3 跨平台术语对照

通用术语 Android iOS Web 嵌入式
应用层 OOM OutOfMemoryError EXC_RESOURCE "Out of memory" 错误 malloc 返回 NULL
系统层 OOM LMK kill jetsam Tab Killer 内存监控守护进程
压力信号 onTrimMemory(level) didReceiveMemoryWarning chrome.system.memory RTOS 内存事件
进程上限 maxHeapSize physFootprintLimit ~4GB/Tab 厂商配置
杀进程策略 LMK 按 oom_adj jetsam 按优先级 + 内存 按 tab 活跃度 按优先级

# 4.4 平台差异点矩阵

维度 Android iOS Web 嵌入式
OOM 抛错 OutOfMemoryError EXC_RESOURCE / NSException OOM JS Error malloc 返 NULL
系统杀进程 LMK(按 oom_adj 评分) jetsam(按优先级 + 内存) Tab Killer 内存守护进程
上限固定 maxHeapSize 安装时定 动态(前台 ~2-3GB) ~4GB/Tab 配置文件
Bitmap 处理 8.0+ 在 Native,单独 RSS NSImage 全在 RAM Image 在 GPU 内存 视框架
压力分级 5 级(COMPLETE / MODERATE / BACKGROUND / UI_HIDDEN / RUNNING_*) 1 级(didReceive...) Chrome 4 级 自定义
后台缓存策略 系统统一管理 应用自管 Service Worker 应用自管

探索性思考:为什么 Android 有 5 级 trim、iOS 只有 1 级 warning?这反映了两个平台的设计哲学差异——Android 给应用"渐进式预警"(让应用按级别响应),iOS 给应用"二元告警"(一旦警告就该全力降级)。没有"更好的设计",只有"更适合的设计" —— Android 的 5 级让灵活性高、iOS 的 1 级让简单性高。


# 05.度量与采集

# 5.1 三类采集方案

OOM 的采集方案有三类,区别在于在 OOM 发生的"前 / 中 / 后"哪一段下钩子:

   平稳期 ──▶ 压力期 ──▶ OOM 触发 ──▶ 进程终止
       │          │           │
       ▼          ▼           ▼
   ① 常态监控   ② 前兆捕获    ③ 现场快照
   (PSS/RSS/   (onTrim/      (UncaughtException
    JS heap)   warning)       / Crash hook)
1
2
3
4
5
6

① 常态监控(采集进程内存基线)

平台 API 间隔建议
Android Debug.MemoryInfo + 定时 30 秒
iOS task_vm_info + Timer 30 秒
Web performance.memory + setInterval 60 秒
嵌入式 getrusage() 等 视场景

物理本质:内存问题是累积的,单点采样无意义,必须看趋势。

② 前兆捕获(监听系统压力信号)

平台 API 触发时机
Android ComponentCallbacks2.onTrimMemory(level) 5 级
iOS UIApplication didReceiveMemoryWarning 一次性信号
Web PressureObserver(实验) 4 级

物理本质:操作系统在内存紧张时通知应用,是治理黄金窗口。

③ 现场快照(OOM 发生时抓 dump)

平台 工具
Android KOOM / Matrix / hprof
iOS MetricKit / 自定义
Web heap snapshot

物理本质:抓 OOM 现场的对象图,用于离线归因分析。

# 5.2 各方案的可见盲区

方案 钩子位置 数据粒度 性能开销 跨端通用性 线上可用 主要局限
① 常态监控 定时采样 总量趋势 低 跨端通用 是 不能定位分配点
② 前兆捕获 系统回调 压力信号 极低 Android+iOS 是 Web 较弱
③ 现场快照 崩溃时 对象图 中 各端有差异 部分 仅崩溃后看

实战建议:① + ② 是线上必备;③ 用于深度归因。

# 5.3 跨平台采集对照表

平台 常态监控 前兆捕获 现场快照
Android Debug.MemoryInfo onTrimMemory KOOM / Matrix
iOS task_vm_info didReceiveMemoryWarning MetricKit MXOOMReport
Web performance.memory PressureObserver Heap Snapshot
Compose 同 Android 同 同
Flutter DevTools Memory 平台原生 DevTools

# 5.4 数据可信度评估

  • 常态监控:可信度高,但只看总量(不是"哪个对象占了")。
  • 前兆捕获:可信度极高(系统级)。
  • 现场快照:可信度高,但有"采集时已晚"的问题。

探索性思考:为什么"前兆捕获"是 OOM 监控最被低估的方法?因为它"不发生时看不到价值"——平时收到 onTrimMemory 处理了就处理了,没有数据上报。但真正的价值在于"前兆响应能让 70% 的 OOM 不发生"(§17.3 实验)。最好的监控不是"事后分析",而是"事前阻止"。


# 06.归因决策树

# 6.1 OOM 归因决策树

   OOM 发生 → 抓现场判断
      │
      ├─ Java 抛 OutOfMemoryError
      │     │
      │     └─ 走 A 类(堆 OOM)分支:
      │        - 看 Java heap usage / Bitmap heap
      │        - 主因:缓存无上限 / 大对象 / 内存泄漏
      │        - 治理:§13 治理一层堆
      │
      ├─ 进程被杀(无栈)
      │     │
      │     └─ 走 B 类(LMK/jetsam)分支:
      │        - 看 onTrimMemory 历史 / jetsam log
      │        - 主因:系统压力大 / 应用未响应压力
      │        - 治理:§14 治理二层 LMK
      │
      ├─ Native crash + signal SIGABRT/SIGSEGV
      │     │
      │     └─ 走 C 类(地址空间)分支:
      │        - 看 /proc/[pid]/maps 最大连续块
      │        - 主因:32 位 + 大块连续分配
      │        - 治理:§15 治理三层地址空间(升 64 位)
      │
      └─ "Too many open files" / "Too many threads"
            │
            └─ 走 D 类(资源耗尽)分支:
               - 看 lsof 计数 / 线程数
               - 治理:§16 资源治理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 6.2 现场快照分析法

Android KOOM 流程:

  1. 监控 Java heap 占用率(> 90% 触发)
  2. fork 子进程(不阻塞主进程)
  3. 子进程 dump hprof
  4. 离线上传 + 分析(找出最大占用对象类)

iOS MetricKit MXOOMReport:

class OOMHandler: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        payloads.forEach { payload in
            payload.crashDiagnostics?.forEach { crash in
                // crash.metaData.osVersion / crash.callStackTree
            }
        }
    }
}
1
2
3
4
5
6
7
8
9

# 6.3 OOM 前兆识别法

前兆指标:

  • onTrimMemory 触发频次(> 3 次/小时即危险)
  • Java heap 占用率持续 > 80%
  • Native 内存增长速率 > 1MB/s(持续)
  • FD 数量 > 800(接近 1024 上限)
  • 线程数 > 200

前兆响应:

  • 立即降级(清缓存)
  • 上报"高危状态"到监控
  • 触发主动重启(§16 兜底)

# 6.4 多维度归因法

按"维度组合"识别 OOM 模式:

维度组合 典型场景 治理方向
32 位 + 长时间使用 短视频、阅读类 App 升级 64 位
低端机 + 后台缓存多 全场景 onTrimMemory 4 级响应
大图加载 + 列表 电商/社交 inSampleSize 降采样 + LRU
视频 + 同时多路 直播、视频会议 单路 + 主动停后台
WebView 多个 Hybrid 应用 WebView 池 + 复用

探索性思考:为什么"决策树 + 维度组合"是 OOM 归因的最佳工具?因为 OOM 是"高维问题"——单一维度(如 PSS)看不到全貌,必须结合 OS 平台 + 设备等级 + 业务场景。多维度归因 = 工程版的"望闻问切" —— 不能只看一个症状下结论。


# 07.堆 OOM 全链路

堆 OOM 是 Java/JS 等"托管语言"独有的 OOM 形态:进程级堆达到上限时抛错。理解堆的全链路,才能精准治理。

# 7.1 Android Java 堆 OOM 全链路

   ① 应用启动
      ↓ Zygote fork
   ② Dalvik/ART 设定 maxHeapSize
      - 默认:128MB / 256MB / 512MB(按设备)
      - largeHeap=true:扩到 256MB / 512MB / 1GB
      ↓
   ③ 应用 new Object()
      ↓
   ④ ART 检查 heap:
      - 够 → 分配
      - 不够 → 触发 GC
      ↓
   ⑤ GC 后还不够:
      - 触发 alloc-time GC
      - 仍不够 → 抛 OutOfMemoryError
      ↓
   ⑥ 应用 catch 或 crash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关键观察:

  • ART 的 GC 是"按需触发",不是定时
  • LargeHeap 只是延后 OOM,不能根治
  • Bitmap 在 Android 8+ 移到 Native heap,单独 RSS

典型代码场景:

// 反例:缓存无上限
private Map<String, Bitmap> cache = new HashMap<>();
cache.put(url, bitmap);  // 永远不清

// 正例:LRU 限上限
LruCache<String, Bitmap> cache = new LruCache<>(maxKb) {
    @Override protected int sizeOf(String key, Bitmap b) {
        return b.getByteCount() / 1024;
    }
};
1
2
3
4
5
6
7
8
9
10

# 7.2 iOS 堆 OOM 全链路

iOS 没有传统意义的"堆 OOM"——所有内存(除 Bitmap GPU 内存)都在统一进程地址空间。但 iOS 的 jetsam 在内存压力时杀进程,效果同 OOM。

   ① 应用 alloc
      ↓
   ② malloc → libmalloc → vm_allocate
      ↓
   ③ 内核分配虚拟内存页
      ↓
   ④ 内核监控 phys_footprint:
      - 接近 jetsam limit → 发 memoryWarning
      - 超过 limit → kill 进程
      ↓
   ⑤ 应用收到 didReceiveMemoryWarning(如果还没被杀)
      → 应用机会响应
1
2
3
4
5
6
7
8
9
10
11
12

关键差异:

  • iOS 没有"堆"概念,是统一虚拟内存
  • jetsam limit 由系统动态决定(前台 ~2-3GB,后台 ~50-200MB)
  • 收到 warning 不响应几乎一定被杀

# 7.3 Web JS 堆 OOM 全链路

   ① V8 启动 Isolate
      ↓
   ② 设定 heap limit(默认 ~4GB / Tab)
      - --max-old-space-size 可调
      ↓
   ③ JS 创建对象
      ↓
   ④ V8 GC(分代 + 标记清除)
      ↓
   ⑤ heap 接近 limit:
      - GC 后仍不够 → "Out of memory" 错误
      → Tab 崩溃 "Aw, Snap!"
1
2
3
4
5
6
7
8
9
10
11
12

Web 特殊性:

  • 浏览器对单 Tab 有限制
  • DOM 节点也消耗内存
  • Image 在 GPU 内存(非 V8 heap)

# 7.4 Compose / SwiftUI 堆 OOM 全链路

  • Compose:使用 Android Java heap(共用),但 LayoutNode 比 View 小约 30%
  • SwiftUI:使用 iOS 统一虚拟内存,View 是值类型(栈或临时堆)

# 7.5 堆 OOM 全链路性能数据

平台 堆上限 OOM 抛错点 是否可恢复
Android maxHeapSize(128-512MB) Java OOMError 可 catch 但风险大
iOS jetsam limit(动态) 进程被杀 不可(已死)
Web ~4GB / Tab "Out of memory" Tab 重载

▶▶ 回扣 §02 案例:短视频 A 类堆 OOM 占 25%——主因是图片缓存无上限。Day 2 加 LRU 上限后立刻 -25%。

探索性思考:为什么 catch OOMError 是反模式?因为 OOM 是"全局信号"——抛错时整个进程都在内存压力下,catch 后再分配很可能再次抛。catch OOM = 治标不治本,正确做法是"前兆响应 + 主动降级"。


# 08.LMK/Jetsam 全链路

当系统级内存压力大时,OS 会主动杀进程释放内存。Android 用 LMK(Low Memory Killer),iOS 用 jetsam。

# 8.1 Android LMK 全链路

   ① 系统检测 free memory
      ↓ 低于阈值(kernel 配置)
   ② LMK 启动评估:
      - 遍历所有进程
      - 按 oom_adj_score 排序(值越大越易被杀)
      ↓
   ③ 杀进程顺序:
      - 缓存进程(CACHED_EMPTY_APP_ADJ = 906)
      - 服务进程(SERVICE_ADJ = 500)
      - 后台进程(PERCEPTIBLE_APP_ADJ = 200)
      - 前台进程(FOREGROUND_APP_ADJ = 0,几乎不杀)
      ↓
   ④ 在杀之前发 onTrimMemory:
      - 应用未响应 → 继续杀
      - 应用响应释放足够 → 暂时不杀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

oom_adj_score 关键值:

进程类型 oom_adj 备注
系统进程 -1000 永不杀
前台进程 0 几乎不杀
前台 Service 100-200 轻微保护
可见后台 200
后台进程 400-900 优先被杀
空进程 906 最先被杀

# 8.2 iOS Jetsam 全链路

   ① 系统检测 phys_footprint
      ↓ 接近物理 RAM 限制
   ② Jetsam 启动评估:
      - 按进程优先级(前台/后台/挂起)
      - 按内存占用
      ↓
   ③ 发 memoryWarning:
      - 前台 App 收到 didReceiveMemoryWarning
      - 应用机会释放
      ↓
   ④ 仍不够 → 杀低优先级进程:
      - 后台 App 优先(< 50-200MB 限制)
      - 挂起 App
      - 前台仅在极端情况下被杀
1
2
3
4
5
6
7
8
9
10
11
12
13
14

iOS jetsam 限制:

状态 内存限制(iPhone 12,6GB RAM)
前台 active ~2-3 GB
后台 active ~200 MB
后台挂起 ~50 MB
Extension ~30-50 MB

# 8.3 onTrimMemory 五级详解

override fun onTrimMemory(level: Int) {
    when (level) {
        TRIM_MEMORY_RUNNING_MODERATE -> {
            // 应用前台运行,系统内存中等
            // 时间窗:> 3 分钟
            // 动作:清理非必要缓存
            imageCache.trimToSize(maxSize / 2)
        }
        TRIM_MEMORY_RUNNING_LOW -> {
            // 系统内存较低
            // 时间窗:~ 2 分钟
            // 动作:积极清理
            imageCache.trimToSize(maxSize / 4)
            videoCache.evictAll()
        }
        TRIM_MEMORY_RUNNING_CRITICAL -> {
            // 系统内存严重不足
            // 时间窗:~ 30 秒
            // 动作:保留最小集
            keepEssentialOnly()
        }
        TRIM_MEMORY_UI_HIDDEN -> {
            // 应用切到后台
            releaseUIResources()
        }
        TRIM_MEMORY_BACKGROUND -> {
            // 后台缓存进程
            // 时间窗:> 10 分钟
            evictBackgroundOnlyCache()
        }
        TRIM_MEMORY_MODERATE -> {
            // 后台 + 系统内存中等
            // 时间窗:~ 3 分钟
            evictMostCache()
        }
        TRIM_MEMORY_COMPLETE -> {
            // 后台 + 系统内存严重不足
            // 时间窗:~ 5 秒
            releaseEverything()
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 8.4 Web Tab Killer 全链路

   ① 浏览器监控总内存(所有 Tab + 渲染进程)
      ↓ 超过阈值
   ② 选择"非活跃 Tab"杀掉:
      - 后台 Tab > 前台 Tab
      - 老 Tab > 新 Tab
      ↓
   ③ Tab 显示"Aw, Snap!" / 灰色占位
      ↓
   ④ 用户切回时浏览器自动重载
1
2
3
4
5
6
7
8
9

Web 特殊性:

  • 没有 onTrimMemory 等价物
  • 应用必须自己监控 performance.memory
  • 后台 Tab 可能被 throttle 或 kill

# 8.5 跨平台 LMK 对照

维度 Android LMK iOS Jetsam Web Tab Killer
触发依据 oom_adj_score 优先级+内存 活跃度
应用预警 onTrimMemory 5 级 1 级 warning 无标准 API
预警时窗 5s-30min 视优先级 几无
应用响应 必做 必做 主动监控

▶▶ 回扣 §02 案例:C 类 LMK 占 20%——主因是 onTrimMemory 未响应。Day 2 接入 5 级响应后立刻 -20%。

探索性思考:为什么 Android 的 5 级 trim 比 iOS 的 1 级 warning"更工程友好"?因为 5 级让应用可以"渐进式响应"——先清非必要,再清次要,最后清核心。渐进式响应优于二元响应 —— 这是工程上的优雅。但代价是复杂度——很多应用懒得做 5 级,结果还不如 iOS。


# 09.地址空间全链路

即使物理内存充足,32 位进程的虚拟地址空间也是硬约束——这是 OOM 治理中最隐形的一类。

# 9.1 32 位虚拟地址空间布局

   32 位进程虚拟地址空间(4GB):
   ┌─────────────────────┐ 0xFFFF_FFFF
   │ 内核空间(1GB)       │ 用户态不可访问
   ├─────────────────────┤ 0xC000_0000
   │ 栈(向下增长)        │
   │   …                 │
   │ 共享库 mmap 区        │ libc.so / libart.so / 各 .so
   │   …                 │
   │ Java heap            │ Dalvik/ART managed heap
   │   …                 │
   │ Native heap          │ malloc 区
   │   …                 │
   │ 数据段 / BSS         │ 全局变量
   │ 代码段 .text         │
   ├─────────────────────┤ 0x0000_0000(实际从一定偏移开始)
   └─────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关键约束:

  • 用户态约 3GB 可用
  • 各区域非连续,碎片化严重
  • 每次 dlopen .so / mmap 文件都会消耗连续大块

# 9.2 地址空间碎片化原因

   时间 t0:剩余 3GB 全部连续
      └────[          可用 3GB           ]
   
   t1:用户加载 100MB 视频
      └─[100MB][       2.9GB           ]
   
   t2:释放 100MB 视频,加载 50MB 缓存(位置不同)
      └──[free][50MB][   2.85GB        ]
   
   t3:反复加载/释放 → 碎片化
      └─[free][50][f][20][f][30][f][f][...]
      最大连续块可能只剩 200MB(远小于剩余 2.85GB)
1
2
3
4
5
6
7
8
9
10
11
12

主要消耗者:

  • Bitmap(Android 8 之前在 Java heap,之后在 Native)
  • mmap 文件
  • dlopen .so
  • Webview 内部缓存
  • 共享内存(IPC)

# 9.3 64 位地址空间

   64 位虚拟地址空间:256TB(实际可用)
   - 几乎不可能用尽
   - 碎片影响可忽略
   - 大块申请几乎总能成功
1
2
3
4

升级 64 位的工程实施:

android {
    defaultConfig {
        ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
    }
    bundle {
        abi { enableSplit = true }  // 按设备分发
    }
}
1
2
3
4
5
6
7
8

注意事项:

  • 所有 native .so 必须有 arm64-v8a 版本
  • APK 体积通常 +30-50MB(双 abi)
  • 但 App Bundle 按设备分发,下载体积不变
  • Google Play 已强制要求 64 位

# 9.4 32 位时代的临时缓解

如果暂时无法 64 位(如某些 .so 没 arm64 版本):

// ① 大文件用 mmap(不占进程虚拟地址连续大块)
val raf = RandomAccessFile(file, "r")
val mapped = raf.channel.map(MapMode.READ_ONLY, 0, file.length())

// ② Bitmap 下采样
val opt = BitmapFactory.Options().apply {
    inSampleSize = 2  // 4x 像素降为 1x
}
BitmapFactory.decodeFile(path, opt)

// ③ 避免单个 > 20MB 大对象,分块处理
1
2
3
4
5
6
7
8
9
10
11

# 9.5 跨平台地址空间对照

平台 默认 ABI 32 位地址空间问题
Android(旧) armeabi-v7a + arm64-v8a 32 位用户有
Android(新) arm64-v8a(Play 强制) 几乎消失
iOS arm64(强制) 无
Web 浏览器内部 Tab 4GB 限制

▶▶ 回扣 §02 案例:B 类地址空间 OOM 占 55%(32 位用户专属)——这是经验派完全没意识到的"隐形 OOM"。Day 4-7 双 abi App Bundle 上线,32 位用户 OOM 直接 -55%。

探索性思考:为什么"地址空间 OOM"在工程界长期被低估?因为它违反直觉——"PSS 200MB / maxHeap 384MB,怎么可能 OOM"。直到工程师看到 /proc/[pid]/maps 中"剩余 1GB 但最大连续块 35MB"的现实,才理解这是物理约束。工程教育的盲区:把"内存够用"等同于"任意大块都能分配" —— 这是错误的。


# 10.资源耗尽全链路

严格意义上不是"内存 OOM",但常被工程师误归类。FD/Thread 耗尽会导致"OOM-like"错误。

# 10.1 文件描述符耗尽

FD 上限典型 1024(每进程)。

消耗者:

  • 文件流(FileInputStream / FileOutputStream)
  • Socket(含 HTTPS)
  • Pipe(IPC)
  • AssetManager
  • Bitmap.decodeFile(短暂占用)

典型崩溃:

java.io.IOException: Too many open files
1

治理:

// ✅ try-with-resources 确保关闭
file.inputStream().use { it.readBytes() }

// ❌ 反例
val input = file.inputStream()  // 忘记 close
val bytes = input.readBytes()
1
2
3
4
5
6

# 10.2 线程数耗尽

每个线程占栈 512KB-1MB。300+ 线程会导致 native OOM。

典型崩溃:

java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
1

治理:

// ❌ 反例:每个任务一个新线程
Thread { task() }.start()

// ✅ 用线程池
val executor = Executors.newFixedThreadPool(4)
executor.submit { task() }
1
2
3
4
5
6

# 10.3 graphics 内存(GPU 内存)

Android 应用的 GPU 内存独立计算:

  • Bitmap(API 26+ 在 native,包含 GPU 上传)
  • SurfaceView / TextureView 帧缓冲
  • HardwareBuffer

治理:

  • 复用 Bitmap(inBitmap)
  • 限制 SurfaceView 数量
  • 主动 recycle 不用的 Bitmap

# 10.4 Web 资源耗尽

Web 的"资源耗尽"通常是:

  • DOM 节点过多(10K+ 即危险)
  • 事件监听器未移除
  • WebSocket 连接未关闭
  • WebGL Context 数量限制(每域名 ~16)

# 10.5 跨平台资源对照

资源 Android iOS Web
FD 上限 1024(典型) 256(默认) N/A
线程数上限 系统默认 ~512 系统决定 Web Worker
GPU 内存 独立 共用 WebGL Context

▶▶ 回扣 §02 案例:D 类资源耗尽未在主因,但案例做了"线程池统一管理"作为防劣化措施。

探索性思考:为什么"资源耗尽"经常被错认为 OOM?因为错误信息里都有 "OutOfMemoryError"。但它们的本质完全不同——堆 OOM 是 Java 对象太多,资源耗尽是 OS 资源到达上限。症状相似 ≠ 根因相同 —— 这是工程归因最容易踩的坑。


# 11.跨端 OOM 全链路

# 11.1 Compose / SwiftUI OOM 全链路

Compose:

  • 使用 Android Java heap(与 View 体系共用)
  • LayoutNode 比 View 体系小 ~30%(Slot Table 优化)
  • Recomposition 不重建对象(diff 复用)

SwiftUI:

  • View 是值类型(栈分配 + 临时堆)
  • ViewGraph 是引用类型,但框架管理生命周期
  • 不会出现"View 泄漏"问题

风险:

  • @State / @Published 的对象引用泄漏
  • Compose remember 的大对象未释放

# 11.2 Flutter OOM 全链路

   Flutter Engine (C++)
      ↓
   Skia Canvas(共享内存池)
      ↓
   Dart Heap (内置 GC)
      ↓
   平台 Surface
1
2
3
4
5
6
7

Flutter 特性:

  • Dart Heap 默认上限不大(200-300MB)
  • Skia 缓存有独立配额
  • ImageCache 默认 100MB(可调)

# 11.3 React Native OOM 全链路

   JS 侧(V8/Hermes Heap)
      ↓ Bridge 序列化
   Native 侧(Java/Swift Heap)
      ↓
   原生 View
1
2
3
4
5

RN 特殊性:

  • 双堆(JS + Native)
  • Bridge 序列化产生临时对象
  • 新架构 (Fabric) 减少双重持有

# 11.4 跨端 Hybrid(WebView)OOM 全链路

   Native Activity / ViewController
      ↓ 持有 WebView 实例
   WebView 内部进程(Android)/ 渲染进程(iOS)
      ↓
   Web 内容(DOM / JS Heap / GPU)
1
2
3
4
5

WebView 风险:

  • 多个 WebView 同时存在 = 内存翻倍
  • WebView 内部缓存不受应用控制
  • iOS WKWebView 在独立进程,但仍计入 jetsam

治理:

  • WebView 池化复用
  • 切后台主动 reload(空白页)
  • 限制同时存在数量

# 11.5 跨端 OOM 对照

框架 主要堆 OOM 风险点
Android Native Java + Native 缓存 / Bitmap
iOS Native 统一 大图 / 多 WebView
Compose Java 同 Android
SwiftUI 统一 同 iOS
Flutter Dart + Skia ImageCache 默认 100MB
React Native JS + Native(双) Bridge + 双重持有
Hybrid WebView 应用 + WebView 多 WebView 共存

探索性思考:为什么 Hybrid 应用的 OOM 治理最复杂?因为它有"双重内存"——Native + WebView 各自一份。优化 Native 不影响 WebView,反之亦然。复合架构带来复合复杂度 —— 工程师必须熟悉两端才能做完整治理。


# 12.跨端对照

# 12.1 五个全链路总览

链路 Android iOS Web Flutter Compose
堆 OOM Java heap (maxHeap) 统一虚拟内存 V8 Heap (~4GB) Dart Heap Java heap
LMK/Jetsam LMK + onTrimMemory jetsam + memoryWarning Tab Killer 平台原生 同 Android
地址空间 32 位有 / 64 位无 64 位强制 Tab 4GB 平台决定 同 Android
资源耗尽 FD 1024 / 线程 FD 256 / 线程 DOM/WebGL 平台 同 Android
兜底 主动重启 系统重启 Tab 重载 应用重启 主动重启

# 12.2 各平台 OOM 优化优先级

Android:

  1. 升级 64 位(双 abi App Bundle)
  2. onTrimMemory 5 级响应
  3. 缓存上限严控
  4. 大图下采样 + inBitmap
  5. 主动重启兜底

iOS:

  1. didReceiveMemoryWarning 必响应
  2. 后台主动释放(< 200MB 限制)
  3. UIImage decode 时降采样
  4. Off-screen Rendering 节制(含图)

Web:

  1. DOM 节点 ≤ 1500
  2. 监控 performance.memory
  3. WebSocket 主动关闭
  4. 长列表虚拟滚动

# 12.3 反直觉问题答疑

问题 答案
物理内存够还 OOM? 32 位地址空间碎片化是物理硬约束
onTrim 一定要释放? 是。每级有不同响应窗(5s-30min)
iOS 没有 OOM? 没有 OOMError,但 jetsam 杀进程效果同
Bitmap OOM = Bitmap 占大? 不一定,可能是 View 层级深
后台被杀 = 内存? 不一定,可能是 ANR / 用户清理
大 Bitmap 拆小还会 OOM? 32 位仍可能(碎片化)
largeHeap 一定避免 OOM? 否,只延后
低端机 OOM 阈值差几倍? 高端机 5-10× 容忍

▶▶ 回扣 §02 案例:经验派 100% 命中"反直觉问题"——把 largeHeap 当万能药。真相是分类施治:堆/LMK/地址/资源四种问题,四种治法。


# 13.治理一层堆

核心命题:治理 A 类堆 OOM 的核心是"为缓存定上限、为大对象定边界"。

# 13.1 缓存上限严控

机理:缓存无上限 = 必然 OOM。

代码:

// LRU 上限 = maxMemory/8
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val cache = LruCache<String, Bitmap>(maxKb) { _, b -> 
    b.byteCount / 1024 
}
1
2
3
4
5

收益:直接消除"缓存无上限型"A 类 OOM。

边界:低端机比例需进一步下调(1/16)。

# 13.2 大对象分块化

机理:单个 50MB+ 对象 = OOM 高风险。改为分片处理,每片 < 5MB。

代码:

// ❌ 大文件全量读
val bytes = file.readBytes()  // 100MB+ 直接 OOM

// ✅ 流式分块读
file.inputStream().use { input ->
    val buffer = ByteArray(8192)
    while (input.read(buffer) > 0) { 
        process(buffer) 
    }
}
1
2
3
4
5
6
7
8
9
10

收益:消除"单对象超大型"OOM。

边界:算法不能简单分块(如全量排序);需要业务支持流式处理。

# 13.3 Bitmap 降采样 + 复用

机理:原图 4000x3000 全量解码占 ~46MB;缩到 800x600 占 ~1.8MB(26× 节约)。

代码:

val opt = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, opt)

opt.inSampleSize = calcInSampleSize(opt, reqWidth, reqHeight)
opt.inJustDecodeBounds = false
opt.inBitmap = bitmapPool.get(opt)  // 复用
val bitmap = BitmapFactory.decodeFile(path, opt)
1
2
3
4
5
6
7
8
9

收益:单图 OOM 降 80% 以上。

边界:复用对 mutable Bitmap 才生效;inBitmap 只能复用相同尺寸。

# 13.4 内存泄漏排查

机理:累积泄漏 = 缓慢的"必然 OOM"。

工具:

  • LeakCanary(Java 对象图)
  • Android Studio Profiler(heap dump 对比)
  • KOOM(线上抓 dump)

常见泄漏源:

  • Activity / Fragment 持有外部引用
  • 静态变量持有 Context
  • 未取消的回调
  • Handler 未 removeCallbacks

# 13.5 配置与监控

largeHeap 慎用:

<application android:largeHeap="true">
1
  • 仅延后 OOM 5-10%
  • 鼓励应用占用更多内存(系统 RAM 压力增大)
  • 不应作为首选方案

探索性思考:为什么"治理一层堆"是最容易做但 ROI 最高的?因为大部分应用根本没设缓存上限——这是"基础不牢"导致的。第一层治理 = 修复基础问题,但因为基础问题太普遍,反而是 ROI 最高的。

▶▶ 回扣 §02 案例:Day 2 缓存上限 + onTrimMemory 5 级响应组合,立刻 -55%。


# 14.治理二层 LMK

核心命题:治理 B 类 LMK 的核心是"响应系统压力信号 + 提升进程优先级"。

# 14.1 onTrimMemory 5 级响应

机理:§17.2 实验证明 onTrimMemory 提供 5 秒-30 分钟响应窗口。

代码(已在 §08.3 详述):

override fun onTrimMemory(level: Int) {
    when (level) {
        TRIM_MEMORY_RUNNING_MODERATE -> imageCache.trimToSize(maxSize / 2)
        TRIM_MEMORY_RUNNING_LOW -> imageCache.trimToSize(maxSize / 4)
        TRIM_MEMORY_RUNNING_CRITICAL -> keepEssentialOnly()
        TRIM_MEMORY_UI_HIDDEN -> releaseUIResources()
        TRIM_MEMORY_BACKGROUND -> evictBackgroundOnlyCache()
        TRIM_MEMORY_MODERATE -> evictMostCache()
        TRIM_MEMORY_COMPLETE -> releaseEverything()
    }
}
1
2
3
4
5
6
7
8
9
10
11

收益:§17.3 实验证明 OOM 率 -70%。

边界:必须分级,一刀切清缓存会让二次进入慢 50%。

# 14.2 iOS didReceiveMemoryWarning

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    
    // 立即释放
    SDImageCache.shared.clearMemory()
    URLCache.shared.removeAllCachedResponses()
    
    // 释放可重建的资源
    if !isViewLoaded || view.window == nil {
        view = nil
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

iOS 关键约束:

  • 5 秒内必须释放(否则被 jetsam)
  • 后台释放更激进

# 14.3 提升前台优先级

机理:LMK 按 oom_adj 杀进程,前台优先级值最低。

代码:

// 关键场景启动前台 Service
val notification = createForegroundNotification()
startForeground(SERVICE_ID, notification)
1
2
3

收益:后台被杀率 -50%。

边界:滥用前台 Service 会被用户嫌弃;只用于必须保活场景(音乐、导航)。

# 14.4 后台主动减负

机理:切后台时主动减负,比等系统通知更安全。

代码:

override fun onTrimMemory(level: Int) {
    if (level == TRIM_MEMORY_UI_HIDDEN) {
        // 切后台立即减负
        clearViewState()
        stopAnimations()
        pauseVideoDecoders()
    }
}

// 或更激进:UI 切后台时序列化关键状态后释放整个 Activity
1
2
3
4
5
6
7
8
9
10

# 14.5 分级降级策略表

压力级别 时间窗 释放策略 用户感知
MODERATE > 3min 半量缓存 无
LOW ~ 2min 1/4 缓存 + 暂停后台任务 几乎无
CRITICAL ~ 30s 仅保留必需 二次进入慢
UI_HIDDEN 无限制 UI 状态释放 无(已切后台)
BACKGROUND > 10min 后台缓存 切回略慢
MODERATE(后台) ~ 3min 大部分缓存 切回较慢
COMPLETE 5s 全部释放 切回需重建

探索性思考:为什么"分级响应"比"一刀切"好这么多?因为分级响应在"内存安全"和"用户体验"之间找到了平衡——一刀切清缓存虽然减内存最猛,但下次进入要全部重建。好的工程方案永远是平衡,而非极端。

▶▶ 回扣 §02 案例:Day 2 接入 5 级响应是 OOM 治理的关键步骤——没有它,后续优化的内存反而会被反复释放/重建造成抖动。


# 15.治理三层地址空间

核心命题:治理 C 类地址空间 OOM 的核武器是"升级 64 位"——其他都是临时解。

# 15.1 升级 64 位(核武器)

机理:§17.4 实验证明 64 位地址空间几乎无限。

代码:

android {
    defaultConfig {
        ndk { 
            abiFilters 'armeabi-v7a', 'arm64-v8a' 
        }
    }
    bundle { 
        abi { 
            enableSplit = true 
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

收益:B 类地址空间 OOM 直接消失,案例 -55%。

边界:

  • 需要所有 native 库提供 arm64 版本
  • APK 体积 +30-50MB(双 abi)
  • App Bundle 按设备分发,下载体积不变
  • 极少数老旧设备(< Android 5)不支持 arm64

# 15.2 大对象用 mmap

机理:mmap 在内核空间映射文件,不占用进程虚拟地址连续大块。

代码:

val raf = RandomAccessFile(file, "r")
val mapped = raf.channel.map(MapMode.READ_ONLY, 0, file.length())
val byte = mapped.get(1024 * 1024)  // 像数组一样访问
1
2
3

收益:大文件操作不消耗虚拟地址连续大块。

边界:

  • 写入需要 msync 显式落盘
  • 崩溃时数据可能未刷盘
  • 仍占用一定虚拟地址空间(但更易复用)

# 15.3 32 位时代的临时缓解

如果暂时无法 64 位(如某些 .so 没 arm64 版本):

① 避免大块(>10MB)分配:

// 反例:一次解码大图
BitmapFactory.decodeFile(path)  // 可能 100MB+

// 正例:先看尺寸再决定
val opt = BitmapFactory.Options()
opt.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, opt)
opt.inSampleSize = calcSampleSize(opt.outWidth, opt.outHeight, 1024, 1024)
opt.inJustDecodeBounds = false
BitmapFactory.decodeFile(path, opt)  // 控制在 < 10MB
1
2
3
4
5
6
7
8
9
10

② 长时间运行后主动重启:

class AppLifecycleObserver {
    fun onAppStart() {
        // 32 位且运行 > 4 小时 → 主动建议重启
        if (is32Bit() && uptimeHours() > 4) {
            promptUserToRestart()
        }
    }
}
1
2
3
4
5
6
7
8

③ 不要频繁创建/销毁大对象:

  • 用对象池
  • Bitmap.recycle() 手动回收(Android 7-)
  • 避免短期大量 dlopen

# 15.4 监控地址空间

Android 32 位监控:

fun getMaxContiguousBlock(): Long {
    val maps = File("/proc/${Process.myPid()}/maps").readLines()
    var maxFreeBlock = 0L
    var lastEnd = 0L
    
    for (line in maps) {
        val (start, end) = parseRange(line)
        val gap = start - lastEnd
        if (gap > maxFreeBlock) maxFreeBlock = gap
        lastEnd = end
    }
    return maxFreeBlock
}

// 阈值:< 50MB 即危险
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 15.5 跨平台地址空间治理对照

平台 主治理手段 备用手段
Android 32 位 升 64 位 mmap + 降采样 + 限大块
Android 64 位 几乎无需治理 仍需基础内存管理
iOS 强制 64 位(无问题) 仅需控总量
Web Tab 分隔(每 4GB) 拆 Worker

探索性思考:为什么"升 64 位"是 Android OOM 治理的"核武器"?因为它一次性解决了 50%+ 的 OOM 类别。升级硬件/平台往往比"软件优化"收益更大 —— 这是工程上经常被忽略的真理(工程师本能想"我能不能用代码解决",但平台升级才是更优解)。

▶▶ 回扣 §02 案例:Day 4-7 双 abi App Bundle 上线 64 位是案例最关键的一步,单步 -55%,远超其他所有手段之和。


# 16.治理四层兜底

核心命题:再完美的治理也无法阻止 100% OOM。第四层提供"主动重启"机制,把"崩溃"变成"短暂卡顿 + 状态保留"。

# 16.1 主动重启机制

机理:§17.5 实验证明主动重启把"崩溃"变"短卡",用户感知崩溃比例 100% → 12%。

代码:

class OomGuard {
    fun checkAndRestart() {
        val javaUsedRatio = Runtime.getRuntime().run { 
            (totalMemory() - freeMemory()).toFloat() / maxMemory() 
        }
        val memInfo = ActivityManager.MemoryInfo()
        activityManager.getMemoryInfo(memInfo)
        
        if (javaUsedRatio > 0.9f || memInfo.lowMemory) {
            saveStateToDisk()  // 保存关键状态
            restartApp()       // 启动新进程并 kill 当前
        }
    }
    
    private fun restartApp() {
        val intent = packageManager.getLaunchIntentForPackage(packageName)
        intent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        startActivity(intent)
        Process.killProcess(Process.myPid())
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 16.2 状态序列化与恢复

关键设计:

  • 哪些状态需要保留?(用户输入、滚动位置、登录态)
  • 哪些可以丢弃?(缓存、临时数据)
  • 序列化到哪里?(SharedPreferences / 本地文件 / Room)
class StateManager {
    fun saveCriticalState() {
        prefs.edit().apply {
            putString("current_page", currentPage)
            putInt("scroll_position", scrollPos)
            putString("draft_text", draftEditText)
            apply()
        }
    }
    
    fun restoreCriticalState() {
        currentPage = prefs.getString("current_page", "home")!!
        scrollPos = prefs.getInt("scroll_position", 0)
        draftEditText = prefs.getString("draft_text", "")!!
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 16.3 重启过渡 UI

避免"白屏"用 splash 衔接:

<!-- styles.xml -->
<style name="SplashTheme">
    <item name="android:windowBackground">@drawable/splash_logo</item>
</style>
1
2
3
4

重启流程:

  1. 检测危险阈值
  2. 保存状态到磁盘
  3. 启动新进程(新进程显示 splash)
  4. kill 当前进程
  5. 新进程启动后恢复状态

用户感知:仅 1-2 秒 splash,无崩溃感。

# 16.4 ROI 排序

ROI 优化项 收益 成本 风险 对应章节
极高 缓存上限 + onTrimMemory 5 级 -55% 1-2 周 低 §13.1 + §14.1
极高 升级 64 位 B 类 -90%+ 2-4 周 中(兼容) §15.1
高 Bitmap 降采样 + 复用 -50% 峰值 1-2 周 低 §13.3
高 大对象 mmap 化 防地址碎片 1 周 低 §15.2
中 OOM 现场抓取 长期防退化 2-3 周 低 §05
中 主动重启兜底 体验改善 90% 2 周 中(状态丢失) §16.1
低 largeHeap 仅延迟 5-10% 1 行配置 低 §13.5

# 16.5 避免反向收益

  • 滥用 largeHeap:治标不治本,鼓励应用占用更多内存
  • catch OOMError:错过真正的根因,下次仍崩
  • 一刀切清缓存:让二次进入慢 50%,用户可能更不满
  • 过度前台 Service:被用户嫌弃 / 系统限制(Android 12+)
  • 重启不保状态:用户感知数据丢失

探索性思考:为什么"主动重启"是最容易被工程师抗拒的方案?因为它"看起来不优雅"——主动 kill 自己的进程。但用户视角下:1.5 秒主动重启 vs 不可控崩溃,主动重启完胜。工程师的"优雅强迫症"经常是用户体验的敌人。

▶▶ 回扣 §02 案例:Day 9-10 主动重启机制,用户感知崩溃比例从 100% 降到 12%——这是最后一公里,但价值巨大。


# 17.求证实验 ⭐

本节是"为什么这些优化生效"的实证基础。每个实验严格遵循"6 步求证法"。

# 17.1 实验一:地址空间碎片

猜想:32 位应用 OOM 都是因为占用过多。

假设:随着应用运行,虚拟地址空间被中小块占用 + 释放后产生碎片,大块分配失败概率随运行时长上升。

设计:

  • 设备:32 位老设备(Android 7,2GB RAM)
  • 测试 App:模拟连续分配 / 释放不同大小(10K–50MB)的块
  • 控制:5 / 30 / 60 分钟
  • 主指标:"尝试 20MB Bitmap 分配成功率"、最大连续块大小

执行:

运行时长 PSS (MB) 最大连续块 (MB) 20MB 分配成功率
0 分钟 80 800 100%
5 分钟 150 380 100%
30 分钟 200 80 75%
60 分钟 220 35 30%

验证:

  • 60 分钟后即使 PSS 只有 220MB,最大连续块只剩 35MB,20MB 分配仅 30% 成功。
  • 64 位应用同样实验:60 分钟后最大连续块仍 > 1GB(影响可忽略)。

思考:

  • 32 位应用的虚拟地址空间碎片是 OOM 的隐形元凶。
  • 运行 30 分钟后即可显著恶化;60 分钟后大 Bitmap 分配成功率可低于 30%。
  • 工程意义:升级 64 位是核武器;32 位时代必须避免大块分配。

# 17.2 实验二:低内存信号时机

猜想:onTrimMemory 是同步信号,几无响应窗。

假设:信号到 OOM 的时间窗与压力级别相关;MODERATE → 60+ 秒,COMPLETE → 5–10 秒。

执行:

onTrimMemory 级别 中位时间到被杀 (s) P95 (s)
TRIM_MEMORY_RUNNING_MODERATE > 600 > 1800
TRIM_MEMORY_RUNNING_LOW 120 600
TRIM_MEMORY_RUNNING_CRITICAL 30 120
TRIM_MEMORY_UI_HIDDEN > 1800 N/A
TRIM_MEMORY_BACKGROUND 600 > 1800
TRIM_MEMORY_MODERATE(后台) 180 480
TRIM_MEMORY_COMPLETE 5 25

验证:

  • MODERATE 后台时窗 3 分钟,应用有充足时间响应。
  • COMPLETE 是"最后通知",5 秒内必须释放,否则被杀。

思考:

  • onTrimMemory 给应用提供 5 秒 – 30 分钟不等的响应窗口。
  • COMPLETE 必须 5 秒内释放;MODERATE 可在 3 分钟内逐步释放。
  • 不同 OEM ROM 阈值差异大(小米 / 华为可能更激进,10 秒就杀)。

# 17.3 实验三:降级策略收益

猜想:降级链路效果有限(< 10%)。

假设:降级链路能显著降低 OOM 率(30–60%)和后台被杀率(50–80%)。

执行(某 App 真实数据):

版本 OOM 率 后台被杀率 首页二次进入速度
v1.0(无降级) 0.42% 18% 800ms
v2.0(基础降级) 0.21% 11% 1200ms(部分缓存丢)
v3.0(完整分级) 0.12% 5% 850ms

验证:

  • v2.0 OOM 率降 50%,但二次进入慢 50%(一刀切清缓存的代价)。
  • v3.0 OOM 率降 71%,且二次进入几乎不受影响。

思考:

  • 完整实现分级降级能让 OOM 率下降 70%、后台被杀率下降 70% 以上。
  • 但必须做"分级",否则会牺牲二次进入体验。
  • 投入 1-2 周做分级降级是高 ROI 工作。

# 17.4 实验四:64 位升级的 OOM 收益

猜想:64 位只是"地址范围"变大,OOM 收益有限。

假设:64 位地址空间几乎无限,B 类 OOM(地址空间碎片)直接消失。

执行:

指标 32 位(v7a) 64 位(arm64)
OOM 率 8.4% 1.2%
Java 堆使用 220 MB 220 MB
最大连续块(30 分钟后) 18 MB > 1 GB
Bitmap 解码失败次数 31 次 0 次

验证:

  • 64 位升级直接消灭 B 类(地址空间)OOM,对 A/C 类无影响。

思考:

  • 64 位升级是 OOM 治理的"核武器"。
  • 实施成本:所有 native .so 提供 arm64-v8a 版本;APK +30-50MB;App Bundle 按设备分发可避免下载膨胀。

# 17.5 实验五:兜底重启的用户体验

猜想:主动重启用户会更不满("自己崩了")。

假设:主动重启比被动崩溃用户体验好——主动重启可以保留状态、平滑过渡。

执行:

指标 A 不主动重启 B 主动重启
真实 OOM 崩溃率 6.8% 0.3%
主动重启率 0% 6.5%
用户感知到崩溃比例 100% 12%
状态丢失比例 100% < 5%

验证:

  • 主动重启把"崩溃"转化为"短暂卡顿 + 状态保留"。
  • 用户体验显著提升。

思考:

  • 重启过程仍有 1-2 秒"白屏",需要 splash 衔接。
  • 必须有完善状态序列化机制。
  • 仅作为最后兜底——主线还是要降低 OOM 触发概率。

# 17.6 五大实验启示

   地址空间碎片         → 32 位 OOM 不只看 PSS         ─┐
   低内存信号时机       → 5 秒到 30 分钟的响应窗       │
   降级策略收益         → 完整分级降级 70% 下降        ├─▶ OOM = 预警 × 降级 × 兜底
   64 位升级            → 直接消灭地址空间类 OOM       │
   主动重启兜底         → 体验从"崩溃"变"短卡"        ─┘
1
2
3
4
5

统一启示:

  • OOM 是分类问题:4 类问题需 4 套治法。
  • 前兆比崩溃更重要:onTrimMemory 是治理黄金窗口。
  • 64 位是核武器:解决 50%+ Android OOM。
  • 分级降级胜过一刀切:兼顾内存与体验。
  • 主动重启是最后防线:用户感知崩溃 100% → 12%。

▶▶ 回扣 §02 案例:方法派 10 天闭环每一步都对应本节实验——Day 1 触发条件分类(§17.1 启示);Day 2 onTrimMemory 5 级(§17.2 + §17.3);Day 4-7 64 位(§17.4);Day 9-10 主动重启(§17.5)。实验是优化前的"必经之路"。


# 18.实战案例

# 18.1 跨端同构案例:分级降级 + 缓存上限

背景:某资讯 App 三端(Android / iOS / Web)OOM 率都在 0.4% 以上。

度量与归因:

平台 优化前 OOM 率 主因
Android 0.42% 缓存无上限 + 32 位地址碎片
iOS 0.18% 后台未释放
Web 0.04% DOM 节点过多

治理(统一逻辑:预警 + 降级 + 兜底):

  • Android:LRU 上限 + onTrimMemory 5 级 + 64 位 + 主动重启
  • iOS:didReceiveMemoryWarning + 后台主动释放
  • Web:performance.memory 监控 + 虚拟滚动减 DOM

效果:

平台 优化后 OOM 率 降幅
Android 0.13% 69%
iOS 0.06% 67%
Web 0.01% 75%

统一启示:OOM 治理跨端通用法则 = 预警 + 降级 + 兜底。

# 18.2 平台特异案例:Android 7 32 位地址空间 OOM

背景:某图片编辑 App 在 Android 7(32 位)OOM 率 0.8%,远高于 8+(0.1%)。

根因:32 位虚拟地址空间碎片化——用户编辑图片时多次 alloc/release 大块内存。

治理:

  • 立即:Bitmap inSampleSize=2 降采样
  • 中期:双 abi 适配,Android 8+ 自动用 64 位
  • 长期:放弃 32 位支持

效果:

  • Android 7 OOM 0.8% → 0.4%(降采样)
  • Android 8+ OOM 0.1% → 0.05%(64 位)

# 18.3 反例案例:catch OOMError 导致后续雪崩

背景:某 App 工程师在 Bitmap 解码处加了 try-catch (OutOfMemoryError),认为"防崩溃"。

结果:

  • OOM 率没降,反而总崩溃率上升
  • 因为 catch 后未释放任何内存,下次分配仍 OOM
  • 而且因为没崩溃日志,问题被掩盖

正确做法:

try {
    val bitmap = BitmapFactory.decodeFile(path)
} catch (e: OutOfMemoryError) {
    // ❌ 仅 catch 不解决根因
    
    // ✅ catch 后立即响应:清缓存 + 降采样重试
    imageCache.clear()
    System.gc()  // 不保证立即生效,但建议
    return BitmapFactory.decodeFile(path, BitmapFactory.Options().apply {
        inSampleSize = 2
    })
}
1
2
3
4
5
6
7
8
9
10
11
12

洞察:catch OOM 不是治理,而是"延迟暴露问题"。真正的治理是在 OOM 发生前响应(onTrimMemory),而不是发生后掩盖。


# 19.防劣化体系

优化不难,难的是"优化后不劣化"。需要"事前预防 + 事中拦截 + 事后回归"三道防线。

# 19.1 三道防线总览

   ┌────────────┐     ┌────────────┐     ┌────────────┐
   │ 编码期 Lint │  →  │ CI 卡口     │  →  │ 线上 SLO    │
   │ IDE 即时提示│     │ Macrobench  │     │ 监控告警    │
   └────────────┘     └────────────┘     └────────────┘
1
2
3
4

# 19.2 编码期 Lint

自定义规则:

  • 缓存类(Map / LruCache / NSCache)未设上限 → 警告
  • 加载 > 10MB 图片直接 decode → 警告(应分块或降采样)
  • 32 位环境分配 > 20MB 连续块 → 警告
  • 未实现 onTrimMemory / didReceiveMemoryWarning → 警告
  • 大对象在 onCreate 同步加载 → 警告
  • catch OOMError 但未释放任何资源 → 错误

# 19.3 CI 卡口

性能基线测试:

// Macrobenchmark MemoryUsageMetric
@Test
fun memoryBenchmark() = benchmarkRule.measureRepeated(
    metrics = listOf(MemoryUsageMetric(MemoryUsageMetric.Mode.Last)),
    iterations = 5
) {
    startActivityAndWait()
    val list = device.findObject(By.res("recyclerView"))
    repeat(50) { list.fling(Direction.DOWN) }
}
1
2
3
4
5
6
7
8
9
10

卡口规则:

  • 内存压测:跑核心场景 30 分钟,OOM 必须为 0
  • 大图压测:构造 100MB+ Bitmap 序列,测试降采样是否生效
  • 后台被杀模拟:触发各级 onTrimMemory,验证降级正确
  • 内存峰值退化 ≥ 10% → 阻断 PR

# 19.4 线上 SLO

核心 SLO 指标:

指标 目标 告警阈值
OOM 率(中端机切片) < 0.1% > 0.3%
后台被杀率 < 5% > 10%
onTrimMemory 触发后内存释放 > 30% < 20%
32 位地址空间最大连续块 > 100MB < 50MB
Bitmap 内存峰值 < 100MB > 200MB

告警 + 自愈:

  • 自动归因:OOM 率告警 → 关联最近 PR
  • 灰度回滚:发现问题自动回滚
  • 错误预算耗尽 → 冻结大内存特性发布

# 19.5 文化建设

  • OOM 预算:新页面必须申报"内存预算"
  • OOM Code Review:内存敏感改动必须有 perf reviewer
  • OOM OKR:OOM 率进 OKR

探索性思考:为什么 OOM 防劣化比"性能防劣化"更难?因为 OOM 是"低概率高代价"事件——平时看不到,出问题就是崩溃。低概率事件的防御需要"压测 + SLO + 告警"三件套 —— 不能等用户上报,要在 CI 阶段就拦住。


# 20.跨平台速查

# 20.1 工具速查

平台 常态监控 前兆捕获 现场快照 离线分析
Android Debug.MemoryInfo onTrimMemory KOOM / Matrix MAT / Profiler
iOS task_vm_info didReceiveMemoryWarning MetricKit MXOOMReport Instruments Allocations
Web performance.memory PressureObserver Heap Snapshot DevTools Memory
Compose 同 Android 同 同 同
Flutter DevTools Memory 平台原生 DevTools DevTools

# 20.2 关键 API 速查

目的 Android iOS Web
限制缓存上限 LruCache + maxMemory/8 NSCache + countLimit Map + manual eviction
监听压力 onTrimMemory didReceiveMemoryWarning PressureObserver
大文件 mmap RandomAccessFile.channel.map mmap() syscall ArrayBuffer (有限)
Bitmap 降采样 inSampleSize UIImage drawSize Canvas drawImage scaled
主动 GC(建议) System.gc() NSCache evictsObjectsWithDiscardedContent manual nullify
进程优先级 startForeground UIApplicationState (无)

# 20.3 各平台 OOM 优化清单

Android:

  • [ ] 缓存类必有上限(maxMemory/8)
  • [ ] 实现 onTrimMemory 5 级响应
  • [ ] 双 abi 上线(armeabi-v7a + arm64-v8a)
  • [ ] App Bundle 按设备分发
  • [ ] Bitmap inSampleSize 降采样
  • [ ] Bitmap inBitmap 复用
  • [ ] 大文件 mmap
  • [ ] OOM 现场抓 hprof(KOOM)
  • [ ] 主动重启兜底
  • [ ] CI 内存压测 30 分钟

iOS:

  • [ ] 实现 didReceiveMemoryWarning
  • [ ] 后台主动释放(NSCache.evictsObjectsWithDiscardedContent)
  • [ ] UIImage decode 时降采样
  • [ ] 限制 WebView 数量
  • [ ] MetricKit 抓 OOM 报告
  • [ ] Instruments 定期 review

Web:

  • [ ] 监控 performance.memory
  • [ ] DOM 节点 ≤ 1500
  • [ ] 长列表虚拟滚动
  • [ ] WebSocket 主动关闭
  • [ ] Image lazy load + 限制并发
  • [ ] WebGL Context 数量限制

# 21.总结与延伸

# 21.1 五条核心原则

  1. OOM 是分类问题:堆 / LMK / 地址空间 / 资源耗尽 四类需四套治法。
  2. 前兆比崩溃重要:onTrimMemory / didReceiveMemoryWarning 是治理黄金窗口。
  3. 64 位是核武器:解决 50%+ Android OOM。
  4. 分级降级胜过一刀切:兼顾内存与体验。
  5. 主动重启是最后防线:用户感知崩溃 100% → 12%。

# 21.2 五个常见误区

误区 真相
"largeHeap 能解决 OOM" 错。仅延后 5-10%,鼓励占用更多内存
"PSS 不高就不会 OOM" 错。32 位地址空间碎片是隐形杀手
"catch OOMError 能防崩溃" 错。未释放资源时下次仍崩
"iOS 没有 OOM" 错。jetsam 杀进程效果同
"OOM 只能事后救火" 错。前兆响应能防 70% OOM

# 21.3 一句话总结

OOM 不是单一问题,而是"堆 / LMK / 地址空间 / 资源耗尽"四类问题的统称。治理 OOM 的核心法则是:分类施治 + 前兆响应 + 分级降级 + 主动重启兜底。其中"升级 64 位"是 Android 的核武器,"onTrimMemory 5 级响应"是降级的基础,"主动重启"是最后防线。预防胜于救火——前兆响应能在 OOM 发生前阻止 70% 的问题。

# 21.4 延伸阅读

  • 卷二·02 内存监控与治理:OOM 是内存治理失败的兜底信号
  • 卷二·01 CPU 监控与分析:内存分配频率影响 GC,间接影响 OOM
  • 卷二·04 线程模型调度优化:线程数耗尽是 D 类 OOM
  • 卷四·03 图片性能解码优化:图片是 OOM 主要来源
  • 卷四·05 功耗与电量优化:内存压力与功耗关联
  • 卷五·01 崩溃捕获设计实践:OOM 是崩溃的一种

下一篇预告:卷二·04 线程模型调度优化 —— 把"线程"这个被滥用的工具用对。

上次更新: 2026/06/07, 10:26:12
内存监控与治理
线程模型调度优化

← 内存监控与治理 线程模型调度优化→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式