是栈迹采集开销大而非长度导致慢;需重写fillinstacktrace()返回this来跳过jvm栈遍历,仅适用于无需诊断的校验异常,注意apm兼容性与框架约束。

Java异常抛出慢,真是因为栈迹太长?
不是栈迹“长”导致慢,而是每次 Throwable 实例化时默认强制采集完整栈迹——这个动作本身开销大,且后续若没被打印或日志捕获,栈迹就纯属浪费。JVM 并不自动“擦除”,所谓“栈迹擦除”是开发者主动放弃采集的优化手段。
- 仅在明确不需要诊断信息的场景下启用,比如业务校验失败快速返回(如参数非法、状态不满足)
- 不要对
IOException、SQLException等需排查外部依赖问题的异常使用 - Java 8 及以前版本无内置支持;Java 9+ 可通过
Throwable#fillInStackTrace()覆盖实现,但更常用的是自定义异常类 + 空实现
怎么写一个不采集栈迹的运行时异常?
核心是重写 fillInStackTrace() 方法并直接返回 this,跳过 JVM 的栈遍历逻辑。注意:必须继承 RuntimeException 或 Error,检查型异常(Exception 子类)无法绕过编译器对栈迹的隐式要求。
public class FastValidationException extends RuntimeException {
public FastValidationException(String message) {
super(message);
}
@Override
public Throwable fillInStackTrace() {
return this; // 关键:不调用 super.fillInStackTrace()
}
}
- 构造函数里仍会走
super(message),但只要不触发fillInStackTrace(),就不会采集栈 - 如果还调用了
initCause()或设置了suppressed异常,需一并重写对应方法,否则可能意外触发栈采集 - 避免在
toString()或printStackTrace()中间接调用fillInStackTrace()—— 这些方法本身不触发,但若父类逻辑有副作用则需验证
用了栈迹擦除,为什么日志里还是看到行号?
日志框架(如 Logback、Log4j2)默认调用 Throwable#getStackTrace() 获取数组,而该数组内容取决于 fillInStackTrace() 是否执行过。如果你看到行号,说明栈迹已被填充——常见原因有:
- 异常被 catch 后又 re-throw,且未用新实例(
throw e;保留原栈,throw new FastValidationException(...);才生效) - IDE 调试时启用了 “exception breakpoint”,JVM 在抛出瞬间强制填充栈迹用于断点定位
- 某些监控 SDK(如 SkyWalking、Pinpoint)会在字节码增强阶段插入栈采集逻辑,与你的重写冲突
- 使用了
Thread.currentThread().getStackTrace()手动采集,这和异常实例无关,不受fillInStackTrace()控制
性能提升到底有多少?要不要上?
单次抛出可快 3–10 倍(取决于栈深度),但真实收益取决于异常发生频率和是否真被 throw 出去。高频校验场景(如 API 网关参数检查、序列化预校验)值得引入;低频或必查根因的异常(如数据库连接失败)加了反而掩盖问题。
立即学习“Java免费学习笔记(深入)”;
- 压测时别只看吞吐量,重点观察 GC 压力下降——因为短生命周期的
StackTraceElement[]对象大幅减少 - 禁止全局替换所有异常为“无栈版”,尤其不能动 Spring 的
RuntimeException子类(如IllegalArgumentException),它们可能被框架内部依赖栈迹做分类处理 - 上线前务必确认 APM 工具兼容性:部分工具靠栈迹做错误聚类,擦除后会导致告警丢失或误合并
真正难的不是写那个 return this;,是怎么在团队里说清:这里不填栈迹是权衡,不是偷懒;而线上突然少了一堆“有用”的堆栈,得有人兜底查问题。









