异常与错误处理:异常抛出与捕获流程、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_pctry块起始指令偏移
end_pctry块结束指令偏移
handler_pccatch块起始指令偏移
catch_type捕获的异常类型(Class索引)

我遇到过一个问题:为什么有时候异常明明被catch了,但堆栈里还是能看到?

因为ART在抛出异常时,会先填充堆栈信息,然后再去查异常表。如果没找到匹配的catch,就往上抛给调用方。这个过程叫「异常展开」(Exception Unwinding)。

1.2 异常捕获:匹配与跳转

ART查找异常处理器的逻辑,说白了就是:

  1. 从当前指令位置开始,查异常表
  2. 看异常类型是否匹配(instanceof 检查)
  3. 匹配成功 → 跳转到handler_pc
  4. 匹配失败 → 弹出当前栈帧,继续查上一级

这里有个性能陷阱。我曾经在项目里看到有人写:

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时,它会做最后一步努力:

  1. 触发一次Full GC(包括软引用、弱引用、虚引用)
  2. 如果还是不够,尝试扩展堆(不超过max heap)
  3. 扩展失败 → 抛出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——死循环了。

四、知识体系总览

下面这张图,把异常处理的整个流程串起来了:

ART异常处理核心流程 ① 异常抛出 athrow指令 → 创建异常对象 ② 查找异常表 遍历Exception Table 类型匹配? ③ 栈帧展开 弹出当前栈帧,查上一级 ④ 生成Stack Trace Thread::CreateStackTrace() ⑤ 跳转到catch块 执行handler_pc OOM特殊处理 触发Full GC → 扩展堆 → 抛出 异常处理三阶段:抛出 → 查找 → 处理(或展开) OOM是特殊异常,触发前会做最后的GC尝试

从这张图你能看到,异常处理不是简单的「抛→接」两步。它涉及异常表遍历、栈帧展开、堆栈生成等多个环节。每个环节都有性能开销,所以别滥用异常

好了,关于异常与错误处理,核心就这些。记住三点:理解流程、避免滥用、优雅降级。下次线上出问题,你至少能看懂堆栈在说什么了。