Exception对象构造时填充堆栈跟踪是主要性能瓶颈,耗时1–5μs且随栈深度增加;RuntimeException子类由JVM内部抛出可避免开销;重写fillInStackTrace()返回this可接近普通对象创建速度。

创建 Exception 对象本身就有明显开销
Java 中抛出异常的性能瓶颈,主要不在 throw 这个动作,而在于构造 Exception 实例时自动填充堆栈跟踪(stack trace)。JVM 会调用 Throwable.fillInStackTrace(),遍历当前线程所有栈帧并生成字符串快照——这个过程涉及大量对象分配和字符串拼接,耗时远超普通对象创建。
- 一个空的
new Exception()在主流 JDK 上平均耗时 1–5 μs(取决于栈深度),比new Object()慢 100 倍以上 - 栈越深、方法嵌套越多,开销越大;在递归或 AOP 代理密集场景下可能飙升到 10+ μs
-
RuntimeException子类(如NullPointerException)在未显式new时由 JVM 内部抛出,不走完整构造流程,所以“免费”——但这不适用于你主动throw new XxxException()
不用堆栈信息时,可以绕过 fillInStackTrace()
如果你只用异常做控制流(比如解析失败提前退出),且完全不需要堆栈定位问题,就能省掉最大开销。标准做法是重写 fillInStackTrace() 方法,让它直接返回 this:
class NoStackException extends RuntimeException {
public NoStackException(String msg) {
super(msg);
}
@Override
public Throwable fillInStackTrace() {
return this;
}
}
- 这种异常实例化耗时接近普通对象(
- 注意:日志里将看不到堆栈,调试时只能靠
msg和外围上下文;建议仅用于明确受控、有替代诊断手段的场景 - 不要对
Exception(检查型异常)这么做——它强制要求调用者处理,掩盖堆栈反而增加维护成本
捕获后重新抛出时,别无脑 throw new XxxException(e)
常见错误是把原始异常 e 当作 cause 传入新异常构造器,看似保留了根因,实则双重开销:既创建了新异常(填栈),又保留了旧异常(也填过栈)。
- 如果只是透传,直接
throw e,JVM 不会重复填充堆栈 - 如果必须换类型(如把
IOException转成业务异常),用带 cause 的构造器没问题,但要接受额外开销;此时可考虑是否真需要换类型,或能否用addSuppressed()补充上下文而非重建 - 日志框架(如 SLF4J)的
logger.error("msg", e)不会触发新异常创建,安全;但logger.error("msg", new RuntimeException(e))就是典型冗余操作
JDK 8u292+ 的 Throwable 构造器优化没你想得那么有用
新版本加了 Throwable(String, Throwable, boolean, boolean) 构造器,支持关掉堆栈和 cause 记录。但实际用起来限制多:
立即学习“Java免费学习笔记(深入)”;
- 第二个
boolean参数控制是否启用 cause,第三个才控制是否填充堆栈;但多数子类(如IllegalArgumentException)没暴露这个构造器,你得自己继承 - 即使关掉堆栈,JVM 仍需做部分栈帧检查(比如判断是否在 native 方法里),开销下降有限(约 30–50%),不如直接重写
fillInStackTrace() - 跨 JDK 版本兼容性差:老版本不识别该构造器,反射调用易出错;生产环境混用 JDK 时风险高
真正高频、低延迟场景(如金融行情处理、实时风控)里,异常应尽量只用于“意外”,而不是“常规分支”。一旦发现 try/catch 出现在热点代码里,优先考虑用返回码、Optional 或状态枚举替代——堆栈填充只是冰山一角,对象分配、GC 压力、JIT 逃逸分析失效才是更隐蔽的拖累。











