Java中Exception对象创建慢是因为每次new都要执行fillInStackTrace()收集完整栈轨迹,涉及遍历调用栈、生成字符串和填充数组;高频创建会导致CPU瓶颈和GC压力。

Java中Exception对象创建为什么慢?
因为每次new一个Exception,JVM都要收集当前线程的完整栈轨迹(stack trace),这个操作涉及遍历调用栈、生成字符串、填充StackTraceElement[]数组——不是简单内存分配。尤其在高频路径(比如循环校验、网络请求预处理)里反复抛异常,CPU会明显卡在Throwable.fillInStackTrace()上。
- 默认构造的
Exception、RuntimeException都会触发栈收集; -
new Exception("msg")和new Exception()开销几乎一样,消息字符串本身不贵,填栈才是瓶颈; - 子类如
IllegalArgumentException也逃不开,除非显式禁用; - JIT可能对极简异常做优化,但别依赖——实测OpenJDK 17下,每秒十万次new仍可见GC压力和延迟毛刺。
哪些场景真该复用Exception实例?
只适用于「语义固定、不携带上下文、不需区分具体失败点」的错误。典型就是参数校验失败:所有id == null都该抛同一个IllegalArgumentException实例,而不是每次new一个。
- ✅ 适合复用:
IllegalArgumentException(空参)、IllegalStateException(状态非法)、自定义的无栈异常(见下节); - ❌ 绝对不能复用:
IOException(需要记录具体文件路径/Socket地址)、SQLException(需含SQL状态码)、任何带动态消息或堆栈用于诊断的异常; - 注意:复用后
e.getStackTrace()永远指向创建位置,不是抛出位置——调试时你会看到“异常在工具类第12行被抛出”,实际出问题的是业务代码第88行。
怎么安全地禁用栈追踪?
继承Exception并重写fillInStackTrace()返回this,就能跳过栈收集。但必须确保这个异常**永不用于诊断**,只作控制流信号。
public class FastIllegalArgumentException extends IllegalArgumentException {
public FastIllegalArgumentException(String msg) {
super(msg);
}
@Override
public Throwable fillInStackTrace() {
return this;
}
}
- 复用这个类的实例是安全的:
private static final FastIllegalArgumentException NULL_ID = new FastIllegalArgumentException("id must not be null");; - 别对
Exception或RuntimeException直接禁用——它们是通用基类,下游可能依赖栈信息; - Spring等框架的全局异常处理器若打印
e.getStackTrace(),会看到空数组,容易误判为“异常没发生”; - Java 19+ 的
VirtualThread环境下,禁用栈对性能提升更明显,但排查协程挂起点会更难。
日志里打异常时,e.printStackTrace()和log.error("msg", e)有啥区别?
前者把栈输出到System.err,后者交由日志框架格式化。关键差异在于:如果异常是禁用栈的(如上面的FastIllegalArgumentException),log.error("msg", e)通常不会报错,但可能只打印类名和消息,栈为空;而e.printStackTrace()会输出空栈,容易让人以为“异常没捕获到”。
立即学习“Java免费学习笔记(深入)”;
- Log4j2 / SLF4J 默认对
null栈做容错,但某些自定义Appender可能NPE; - 如果用
log.error("id={} invalid", id, e),务必确认e不是复用的无栈实例——否则日志里看不到根因; - 线上环境建议统一用
log.error("msg", e),避免printStackTrace()绕过日志级别和异步缓冲。
最易被忽略的点:异常复用只解决创建开销,不解决抛出成本。JVM的异常抛出本身就有分支预测失败、栈帧清理等开销。真要极致性能,得用返回码或Optional替代——但那就不是异常处理了。











