Java中Exception构造开销大,因fillInStackTrace()同步采集栈轨迹,比普通对象创建慢10–100倍;应避免用异常作控制流、循环内新建异常,优先用if判断或复用静态异常,自定义异常可重写fillInStackTrace()跳过栈采集。

Java中Exception构造本身就有明显开销
每次新建Exception对象,JVM都会采集当前线程的栈轨迹(stack trace),调用Throwable.fillInStackTrace()——这个操作是同步且昂贵的,尤其在高并发或高频路径上。实测显示,构造一个Exception比普通对象创建慢10–100倍,取决于栈深度。
常见误用场景:
- 用
new RuntimeException("xxx")做控制流(比如校验失败时抛出再捕获) - 在循环内反复构造异常(如解析CSV时每行都
new ParseException()) - 自定义异常未重写
fillInStackTrace(),却声称“轻量”
优化建议:
- 对已知可预期的错误(如参数为空、格式不符),优先用
if判断 + 返回错误码/封装Optional/Result类,避免抛异常 - 若必须抛异常,复用静态异常实例(仅适用于不带上下文信息、无栈需求的场景,如
public static final IllegalArgumentException ILLEGAL_ARG = new IllegalArgumentException();) - 自定义异常时,在构造函数中显式调用
super(null)并重写fillInStackTrace()返回this,可跳过栈采集(注意:这样会导致printStackTrace()无堆栈,仅适用于内部状态机等可控场景)
try-catch块本身不拖慢性能,但catch分支执行有代价
JVM对try块的编译处理已很成熟:只要没真正抛出异常,try-catch的字节码和纯代码几乎无差异。瓶颈永远在“异常被抛出并被捕获”那一瞬间——即throw触发栈展开(stack unwinding)+ catch块内逻辑执行。
立即学习“Java免费学习笔记(深入)”;
典型陷阱:
- 把整个方法包进一个大
try-catch,以为“兜底安全”,结果掩盖了本该快速失败的逻辑 -
catch块里做耗时操作(如日志序列化、远程调用、加锁),放大单次异常的延迟 - 捕获
Exception或Throwable后不做区分,统一处理,导致本可忽略的NullPointerException也走重试逻辑
实操建议:
- 缩小
try范围,只包裹真正可能抛异常的语句(例如仅Integer.parseInt(str),而非整段业务逻辑) -
catch块内避免任何阻塞或复杂计算;记录日志用logger.debug("msg", e)而非e.printStackTrace()(后者会同步刷IO) - 按异常类型分层捕获:
catch (NumberFormatException e)做转换兜底,catch (IOException e)走重试,不要一锅炖
使用Throwable.addSuppressed()需警惕内存与GC压力
在try-with-resources或手动close()时,如果主异常已存在,后续close()抛出的异常会通过addSuppressed()附加。这看似优雅,但每个被抑制的异常都保留完整栈帧,且引用链会延长主异常生命周期。
问题现象:
- 频繁打开关闭资源(如短连接HTTP客户端),导致大量
SQLException或IOException被抑制,堆内存中积累大量Throwable对象 - 使用堆外内存或DirectByteBuffer的场景下,这些异常对象间接延长了Cleaner关联对象的存活时间,触发更早的Full GC
应对方式:
- 确认是否真需要保留抑制异常——多数情况下,仅记录
close()失败的日志即可,无需附加到主异常 - 若必须保留,考虑在
catch块中显式调用suppressed.getStackTrace()截取前N帧,再清空原suppressed引用(需反射绕过私有字段,慎用) - 监控应用中
java.lang.Throwable实例的GC频率,用jstat -gc或Arthas观察EU(Eden区使用量)突增是否与异常密集发生时段吻合









