## 第4章 崩溃类型判断矩阵:空指针、数组越界、资源泄漏、ANR、内存耗尽、非法指令、信号异常 ### 4.1 现象识别——崩溃表象的“第一眼”分类 崩溃现场的第一线索往往隐藏在日志、堆栈和系统行为的组合中。快速将表象映射到七种类型之一,能大幅缩短后续定位链路。 | 崩溃类型 | 典型现象 | 系统表现 | |---------|---------|---------| | **空指针** | `NullPointerException` / `EXC_BAD_ACCESS` | 无额外系统日志,进程直接退出 | | **数组越界** | `ArrayIndexOutOfBoundsException` / 段错误(越界写) | 可能伴随 `SIGSEGV`,堆栈指向数组操作函数 | | **资源泄漏** | 文件描述符耗尽 / 数据库连接满 / 句柄数持续增长 | `Too many open files` / `Out of handle` | | **ANR** | 界面卡死5秒以上 / 主线程无响应 | 系统生成 `traces.txt` / `anr_report` | | **内存耗尽** | `OutOfMemoryError` / `malloc failed` | 系统 `lowmemorykiller` 触发,多伴随内存快照 | | **非法指令** | `IllegalInstruction` / `SIGILL` | CPU 执行了未定义指令,多见于 JIT 编译错误或 CPU 不兼容 | | **信号异常** | `SIGABRT` / `SIGSEGV` / `SIGFPE` | 进程收到操作系统信号,堆栈往往指向 libc 或运行时库 | >
**诊断口诀**:看堆栈里最近一次自己的代码调用——空指针找中间变量,越界找循环边界,ANR 等主线程,内存耗尽看分配点。
### 4.2 数据采集——为矩阵赋值收集证据 每一种崩溃类型需要针对性采集的关键字段不同。以下排查表列出了必采集项: | 崩溃类型 | 必采集日志 | 采集合集 | 采样工具 | |----------|------------|----------|----------| | 空指针 | 异常栈 + 变量值快照 | 调用链全部类名、方法名、行号 | `Exception.getStackTrace()` / `crash_dump` | | 数组越界 | 数组长度 + 访问下标 | 循环变量现场值、入参索引 | `-Xcheck:jni`(Java) / AddressSanitizer(C++) | | 资源泄漏 | 打开文件/句柄列表 | 资源创建时间、线程ID、调用栈 | `lsof` / `strace -e open` / LeakCanary | | ANR | 主线程堆栈 + CPU/IO耗时 | 所有线程堆栈快照、binder 事务 | `dumpsys activity.anr` / SIGQUIT | | 内存耗尽 | Heap Dump + 分配栈 | 大对象分配点、GC 日志 | `MAT` / `hprof` / JNI 指针泄漏检测 | | 非法指令 | CPU 寄存器 + 指令码 | JIT 缓存内容、汇编片段 | `objdump` / `gdb disassemble` | | 信号异常 | 信号编号 + si_code | 发送信号时的内核上下文 | `sigtrap` / `syscall` 跟踪 | ### 4.3 原因定位——判断矩阵与故障树 以下故障树展示了从日志锚点出发的遍历逻辑: 崩溃类型判断矩阵故障树 崩溃现象日志入口 判断:堆栈长度 & 信号类型 有明确异常栈 无明确异常栈 NullPointerException OutOfMemoryError ArrayIndexOutOfBounds IllegalInstruction ANR (看主线程) 信号异常 (SIGABRT等) 资源泄漏 (慢速持续) 内存耗尽 (无Java栈) 确认崩溃类型 + 根因方向 判断规则说明 • 先看堆栈顶部是否有自己的代码(有 → 左枝),没有或只剩系统库 → 右枝。 • 右枝中 ANR 需同时检查 CPU/IO/锁;信号异常看 si_code。 • 左枝中空指针往往伴随明确异常类名;内存耗尽看 OOM 类型;越界看索引范围。 • 非法指令罕见,检查 CPU 兼容性与 JIT 优化开关。 ### 4.4 修复动作——每种类型的定向处理 | 崩溃类型 | 修复动作 | 常见反模式 | |----------|----------|-----------| | 空指针 | 加空值保护、使用 `Optional`/提早 fail-fast | 只加 `if(x!=null)` 但不追溯根因 | | 数组越界 | 修复边界逻辑、使用安全集合(`ArrayList`、`std::vector::at`) | 只 catch 异常不修复循环 | | 资源泄漏 | 确保 `close()` 在 finally/RAII 中、使用池化 | 只增 `System.gc()` 不释放 | | ANR | 异步化耗时操作、锁优化、减少 I/O | 用 `new Thread` 但导致线程爆炸 | | 内存耗尽 | 减少对象引用、检测大对象、使用 WeakReference | 只调大堆限制不治理分配 | | 非法指令 | 更新 CPU 微码、关闭 JIT 特定优化、重新编译 | 忽略并继续部署 | | 信号异常 | 检查空指针后再调用、修复 fstat/写越界(SIGSEGV) | 只 catch 不恢复 | ### 4.5 复测——验证修复的“击杀确认” 每个崩溃类型复测时应使用不同的验证手段: - **空指针**:所有空值路径单元测试覆盖,mock 空对象返回。 - **数组越界**:边界值测试(长度为 0、1、max、max+1)。 - **资源泄漏**:压力测试后检查 `/proc/self/fd` 计数,运行 24h 无增长。 - **ANR**:用 Systrace 或 `atrace` 检测主线程耗时,目标 < 100ms。 - **内存耗尽**:OOM 场景压力测试,使用 `-Xmx` 收紧堆大小验证无崩溃。 - **非法指令**:在不同 CPU 架构(arm64/x86_64)上运行原生测试集。 - **信号异常**:monkey 测试 + 信号捕获(`sigaction`)确认不再出现。 ### 4.6 预防——矩阵驱动的常态化防御 >
**核心原则**:不要在运行时防御已经能通过编译期/静态分析消除的崩溃。
| 预防纵深 | 对应类型 | 工具/实践 | |----------|----------|-----------| | 编译期检查 | 空指针、数组越界 | `@NonNull`/`@Nullable` 注解、SpotBugs、Clang Static Analyzer | | 静态分析 | 所有类型 | FindBugs、Infer(Meta)、PVS-Studio | | 单元测试 | 空指针、越界、ANR | 边界值用例 + 异步 timeout 测试 | | 动态检测 | 内存耗尽、资源泄漏 | LeakCanary(Android)、AddressSanitizer(C++) | | 代码 Review | 非法指令、信号异常 | 每一处 native 调用检查参数有效性 | | 发布看板监控 | ANR、OOM | 实时 Crash Dashboard 按类型聚合 | 结合本章的判断矩阵,团队可以在问题发生的最初10秒内选出正确的排查路径,避免在高声量崩溃中走错分叉。