创建异常对象时性能开销主因是fillInStackTrace()采集调用栈,而非内存分配;其耗时与栈深度近似平方增长,且new时即触发,不可惰性延迟;可通过复用异常、覆写fillInStackTrace或改用if校验来优化。

创建异常对象时生成堆栈轨迹(Stack Trace)是性能开销的主因,不是因为分配内存本身,而是因为 JVM 必须实时采集并格式化当前线程的完整调用链信息。
fillInStackTrace() 是 CPU 开销的核心源头
所有 Java 异常类(如 Exception、RuntimeException)在构造时,默认会调用 Throwable 的构造函数,进而触发 native 方法 fillInStackTrace()。这个方法不返回值,但会强制执行以下操作:
- 暂停当前线程执行,遍历整个调用栈的每一帧(stack frame)
- 对每个栈帧提取类名、方法名、字节码偏移量、源文件名和行号
- 将这些信息逐层组装成字符串数组,并缓存到异常对象的
stackTrace字段中 - 该过程涉及大量反射调用、字符串拼接、类加载器查找和内存拷贝,无法被 JIT 编译器有效内联或省略
栈深度越大,CPU 耗时越非线性增长
实测表明,fillInStackTrace() 的耗时与当前调用栈深度呈近似平方关系——不只是“帧数多”,还因每帧需解析符号信息,而深层调用往往伴随更多匿名类、Lambda 表达式或动态代理,进一步拉高解析成本:
- 栈深 5 层:平均约 0.1–0.3 ms
- 栈深 20 层:平均约 1.5–4 ms(是前者的 10 倍以上)
- 若方法被 JIT 编译优化(如内联),部分栈帧可能被折叠,但 fillInStackTrace() 仍会尽力还原可读路径,无法跳过关键帧
即使不抛出,开销已在 new 时发生
很多人误以为“只要没 throw,就没事”。实际上,new RuntimeException() 这一行代码执行完毕,fillInStackTrace() 就已完成,CPU 时间已被消耗:
- 该行为不可被 JVM 惰性延迟(除非使用自定义 Throwable 子类并重写 fillInStackTrace() 返回 this)
- 日志语句如
log.error("failed", new IOException())同样触发完整栈采集,且后续 log 框架还会再次调用printStackTrace(),造成重复开销 - JIT 在极少数场景下可能对空异常(如仅用于占位)做栈轨迹截断,但不可预测、不可依赖
如何绕过或降低这一开销
关键不是避免 try-catch,而是避免无谓创建异常对象:
- 高频校验逻辑(如参数非空、集合存在性)改用 if + 返回值 / Optional / 断言工具类
- 必须抛异常时,复用静态 final 异常实例(适用于无上下文信息的场景,例如
private static final IllegalStateException ILLEGAL = new IllegalStateException();) - 需携带动态消息时,先拼好字符串再传入构造器,避免在 fillInStackTrace() 执行期间重复计算:
throw new IllegalArgumentException("id=" + id + " not found"); - 继承 Throwable 并覆写
fillInStackTrace()方法,直接 return this(适合自定义业务异常,且明确不需要堆栈信息)











