抛出异常开销极大,因需生成栈追踪信息并分配内存;应避免用异常做流程控制,而用isPresent()、正则校验等替代方案。

抛出异常本身开销很大
Java 中 throw 一个异常的代价远高于普通控制流跳转。JVM 需要收集当前线程的完整栈帧信息(即生成 StackTraceElement[]),这个过程涉及遍历调用栈、反射查找类名和行号,还会触发内存分配。即使你捕获后立刻吞掉,构造异常对象这一步已经发生。
- 不抛异常时,
try-catch块本身几乎零开销(HotSpot 会做优化,不会插入额外指令) - 但只要执行到
throw new RuntimeException(),哪怕空参构造,也比return null慢 10–100 倍(视栈深度而定) - 常见误用:
Integer.parseInt("abc")抛NumberFormatException来做“判断是否为数字”——这是典型反模式
不要用异常做流程控制
把异常当 if-else 用,比如靠 catch (NoSuchElementException) 来判断集合是否为空,或用 catch (ParseException) 来试探字符串格式,这类写法在高并发或高频路径下会迅速拖垮性能。
- 正确替代:对
Optional用isPresent();对数字解析,先用正则或Character.isDigit()快速筛一遍 -
Map.get(key)返回null是正常行为,不应期待它抛异常;真要区分“没找到”和“值为 null”,该用Map.containsKey()或getOrDefault() - 日志框架里也常见陷阱:写
log.debug("value={}", obj.toString()),若obj为null会抛NullPointerException—— 应改用log.debug("value={}", Objects.toString(obj))
异常捕获范围与性能无关,但影响可维护性
catch (Exception e) 和 catch (IOException e) 在字节码层面生成的异常表条目是一样的,JVM 查找 handler 的开销不因类型宽泛而增加。但过度宽泛的 catch 会掩盖真正需要关注的问题。
- 避免
catch (Throwable t),它会捕获OutOfMemoryError等不可恢复错误,导致程序状态不一致 - 如果方法声明了多个受检异常(如
read() throws IOException, InterruptedException),用多 catch 块分别处理比统一 catch 后用instanceof判断更清晰,也便于未来拆分逻辑 - 注意:Java 7+ 支持多异常捕获语法
catch (IOException | SQLException e),语义等价于两个独立 catch,无性能差异
自定义异常要不要重写 fillInStackTrace?
如果你的异常**永远不用于诊断问题**(比如内部状态机的控制信号),可以重写 fillInStackTrace() 直接返回 this,跳过栈追踪收集。但这属于极端优化,需谨慎。
立即学习“Java免费学习笔记(深入)”;
public class ControlFlowException extends RuntimeException {
@Override
public Throwable fillInStackTrace() {
return this;
}
}
- 仅适用于明确知道调用栈无调试价值的场景,例如协程调度、状态跳转
- 别对
IllegalArgumentException或NullPointerException这类通用异常这么做,它们是 JVM 和工具链依赖的诊断信号 - 启用
-XX:+OmitStackTraceInFastThrow(HotSpot 默认开启)可让 JVM 对某些重复异常省略栈信息,但该优化不覆盖所有情况,不能替代设计上的规避











