应使用 System.nanoTime() 配合 try-finally 统计异常处理耗时,因它能完整覆盖异常构造、栈展开、finally 执行等全路径;而仅在 try-catch 内计时不准确,会遗漏栈遍历和 finally 开销。

在异常处理中统计“执行耗时”,关键不是只测正常流程,而是要准确捕获异常发生时的完整开销——包括异常构造、栈遍历、try-catch 匹配、finally 执行等。用 System.nanoTime() 配合 try-finally 是一种轻量、高精度、且能覆盖异常路径的可靠方式。
为什么不用 try-catch 块内计时?
如果把 start = System.nanoTime() 放在 try 前、end 放在 catch 里,会漏掉两个关键阶段:
- 异常抛出后、进入
catch前的 JVM 栈展开(stack unwinding)耗时 - 与该
try关联的finally块的执行时间(它一定会运行,无论是否异常)
而 finally 中的逻辑(如资源清理、日志记录、监控上报)恰恰是异常路径的真实开销组成部分。
正确姿势:计时范围包裹整个 try-finally 结构
将计时起点放在 try 前,终点放在 finally 末尾,确保无论是否抛异常,耗时都包含完整的控制流路径:
long start = System.nanoTime();
try {
doSomething(); // 可能抛异常
} finally {
long costNs = System.nanoTime() - start;
logIfSlow(costNs); // 或上报 metrics
}这样,即使 doSomething() 抛出 NullPointerException,finally 仍执行,costNs 就真实反映了“从开始到清理完成”的总延迟。
注意点:避免在 finally 中干扰计时逻辑
finally 应尽量轻量,尤其不能在其中再调用可能抛异常或阻塞的方法(如远程日志服务),否则会污染耗时数据,甚至引发二次异常掩盖原异常。建议:
- 仅做本地计算、条件判断、异步提交(如用队列缓存后由后台线程上报)
- 避免在
finally里调用System.nanoTime()多次——单次差值已足够 - 若需区分“异常 vs 正常”路径,可在
catch中设标志位,finally读取并打标上报
对比:Throwable.getStackTrace() 不适合性能统计
有人想通过捕获异常后调用 e.getStackTrace().length 或遍历栈帧来估算开销,这是误导性的:
- 栈轨迹生成本身开销大,且受 JVM 参数(如
-XX:-OmitStackTraceInFastThrow)影响 - 它反映的是“错误现场快照成本”,不是“异常传播+处理+清理”的端到端延迟
- 无法覆盖 finally 的执行时间,也无法体现 JIT 优化对异常路径的实际影响
真正关心“这段代码出问题时用户等了多久”,就该用 nanoTime + try-finally 端到端测量。

















