异常与错误处理:异常抛出与捕获流程、Stack Trace生成、OOM处理
异常处理,说白了就是程序出问题时的「善后机制」。
我在做Android系统开发的头两年,总觉得异常处理就是写几个try-catch。直到有一次线上崩溃率飙升,我才发现——不懂底层异常机制,你连崩溃日志都看不懂。今天我们就从ART的角度,把异常这件事彻底讲透。
一、异常抛出与捕获流程
Java层的异常,在ART里是怎么流转的?
嗯,这个过程其实分三步:抛出 → 查找 → 处理。
1.1 异常抛出:从字节码到Native
当你在Java代码里写 throw new RuntimeException() 时,编译器会生成一条 athrow 指令。ART执行这条指令时,会调用 Throwable::initCause() 来初始化异常对象,然后进入异常处理流程。
关键点来了——ART不会立即跳转到catch块。它会先做一件事:遍历当前方法的异常表(Exception Table)。
异常表结构(每个方法都有):
| 字段 | 说明 |
|---|---|
| start_pc | try块起始指令偏移 |
| end_pc | try块结束指令偏移 |
| handler_pc | catch块起始指令偏移 |
| catch_type | 捕获的异常类型(Class索引) |
我遇到过一个问题:为什么有时候异常明明被catch了,但堆栈里还是能看到?
因为ART在抛出异常时,会先填充堆栈信息,然后再去查异常表。如果没找到匹配的catch,就往上抛给调用方。这个过程叫「异常展开」(Exception Unwinding)。
1.2 异常捕获:匹配与跳转
ART查找异常处理器的逻辑,说白了就是:
- 从当前指令位置开始,查异常表
- 看异常类型是否匹配(
instanceof检查) - 匹配成功 → 跳转到handler_pc
- 匹配失败 → 弹出当前栈帧,继续查上一级
这里有个性能陷阱。我曾经在项目里看到有人写:
try {
// 大量业务逻辑
} catch (Exception e) {
// 空的catch块
}
千万别这么干! 空的catch块会让ART白白做一次异常表遍历,而且异常对象不会被回收,直到GC介入。更可怕的是——你吞掉了错误,后面排查问题根本找不到原因。
避坑指南:我曾经接手过一个模块,线上OOM频发,查了半天发现是某个try-catch里捕获了OutOfMemoryError却什么都没做。嗯,这等于把炸弹藏在了地毯下面。
二、Stack Trace生成:谁在背后干活?
你每次看到的那一串「at com.example...」堆栈信息,不是凭空产生的。
ART里,堆栈生成的核心是 Thread::CreateStackTrace()。它会做这几件事:
- 遍历当前线程的栈帧(从顶到底)
- 对每个栈帧,解析出方法名、文件名、行号
- 把这些信息组装成
StackTraceElement[]
你想想看,如果堆栈有100层,ART就要做100次「栈帧→方法信息」的映射。这就是为什么异常抛出的性能开销很大。
我记得有一次优化启动速度,发现某个库在初始化时频繁抛异常做流程控制。我当时的建议是:别拿异常当goto用。一次异常抛出的耗时,大约是普通方法调用的100倍以上。
2.1 行号映射:谁在帮你定位代码?
堆栈里的行号,来自编译时生成的 LineNumberTable。ART通过 DexFile::GetLineNum() 来查这个表。
但有个坑——如果开启了混淆(ProGuard/R8),行号可能会变。我建议线上版本一定要保留mapping文件,否则你看到的堆栈行号全是错的。
小技巧:在Android 8.0以上,ART支持了 -XX:+ShowHiddenFrames 参数,可以显示内部调用栈。调试疑难问题时很有用。
三、OOM处理:当内存真的不够了
OOM(OutOfMemoryError)是所有Android工程师的噩梦。但你知道吗?OOM的触发时机,比你想象的要早。
3.1 OOM的触发条件
ART里,OOM不是在内存用尽时才抛出的。它有两个触发点:
| 触发点 | 条件 | 说明 |
|---|---|---|
| 分配时 | 堆内存不足,且GC后仍不足 | 最常见的OOM场景 |
| 分配前 | 单次分配超过堆上限 | 比如一次分配500MB |
我遇到过最诡异的一次OOM:App在低端机上频繁崩溃,堆栈指向Bitmap创建。查了内存快照发现——堆里还有200MB空闲。为什么还会OOM?
原因在于:连续内存碎片化。ART的堆是分代管理的,年轻代和老年代之间有空隙。虽然总空闲内存够,但找不到一块连续的区域来放Bitmap。这就是所谓的「内存碎片导致的OOM」。
3.2 ART的OOM处理策略
当ART决定抛出OOM时,它会做最后一步努力:
- 触发一次Full GC(包括软引用、弱引用、虚引用)
- 如果还是不够,尝试扩展堆(不超过max heap)
- 扩展失败 → 抛出OutOfMemoryError
这里有个细节:ART不会在OOM时立即杀死进程。它只是抛出一个异常,让应用层自己决定怎么处理。但大多数App的默认行为是——崩溃。
核心观点:OOM不是「内存用完了」,而是「ART认为你无法再分配了」。这个判断基于堆大小、碎片率、GC频率等多个因素。
3.3 如何优雅处理OOM?
我个人习惯在关键路径上做两件事:
- 捕获OOM:在加载大图、解析大文件时,用try-catch包一下
- 降级处理:捕获到OOM后,释放缓存、降低分辨率、提示用户
我曾经在图片库中这样处理:
try {
bitmap = BitmapFactory.decodeStream(input);
} catch (OutOfMemoryError e) {
// 降级:用一半的采样率重新加载
options.inSampleSize = 2;
bitmap = BitmapFactory.decodeStream(input, null, options);
}
嗯,这里要注意:捕获OOM后,最好主动调用一次System.gc(),虽然不保证立即执行,但能加快内存回收。
避坑指南:千万不要在catch块里做大量内存分配。我曾经见过有人在OOM时尝试创建日志对象,结果又触发了一次OOM——死循环了。
四、知识体系总览
下面这张图,把异常处理的整个流程串起来了:
从这张图你能看到,异常处理不是简单的「抛→接」两步。它涉及异常表遍历、栈帧展开、堆栈生成等多个环节。每个环节都有性能开销,所以别滥用异常。
好了,关于异常与错误处理,核心就这些。记住三点:理解流程、避免滥用、优雅降级。下次线上出问题,你至少能看懂堆栈在说什么了。