不能直接用 System.currentTimeMillis() 做精确耗时统计,因其受系统时钟调整影响会导致耗时计算错误;应使用 System.nanoTime(),它单调递增、不受时钟干扰,适合性能打点等场景。

为什么不能直接用 System.currentTimeMillis() 做精确耗时统计
因为 System.currentTimeMillis() 返回的是系统当前时间戳(毫秒级),受系统时钟调整影响——比如 NTP 同步、手动改时间、虚拟机休眠等,都可能导致返回值“倒退”或“跳变”,算出来的耗时可能是负数或远大于真实值。
它适合记录日志时间、生成唯一 ID 等对绝对时间有要求但不苛求精度的场景,不适合测量代码执行耗时。
常见错误现象:
– 方法明明只跑了几毫秒,却打印出 -2345 或 120000 毫秒
– 在容器环境或云主机上结果波动极大,无法复现
该用 System.nanoTime() 替代,但要注意它的局限性
System.nanoTime() 返回的是纳秒级单调递增计数器,不受系统时钟调整影响,是测量耗时的正确选择。
使用场景:
– 方法级性能打点
– 微服务链路中某段逻辑的耗时采集
– 单元测试里验证方法是否在预期时间内完成
实操建议:
– 一定要在同一 JVM 进程内配对调用,不能跨线程传递起始值(除非你明确知道线程调度行为)
– 不要把它当“时间”用(比如转成 Date 或格式化),它没有日期语义
– 结果单位是纳秒,除以 1_000_000 得毫秒,注意整型溢出风险(推荐用 double 或 long 保持精度)
示例:
long start = System.nanoTime();<br>doBusiness();<br>long costMs = (System.nanoTime() - start) / 1_000_000;
高并发下别手写耗时统计,优先走 AOP 或标准埋点框架
手动在每个方法前后加 System.nanoTime() 容易漏、难维护,且无法统一处理异常路径(比如方法抛异常时没记录结束时间)。
实操建议:
– Spring 项目用 @Around 切面,在 proceed() 前后取 nanoTime,并用 try/catch/finally 保证结束时间一定被记录
– 使用 Micrometer + Prometheus 时,直接用 Timer.recordCallable(),底层已封装好 nanoTime 逻辑和统计维度
– 避免在循环体内反复调用 nanoTime(),它本身有微小开销(通常
注意 JVM 预热和 JIT 编译对首次测量的干扰
第一次运行某个方法时,JIT 可能还没介入,测出来的时间偏长;连续多次调用后,JIT 优化生效,耗时骤降。这会让 System.nanoTime() 的原始数据看起来“不稳定”。
这不是计时方式的问题,而是测量对象本身在变化。
实操建议:
– 性能对比实验前,先预热方法(比如空跑 10000 次)
– 单次测量价值有限,应统计多次运行的中位数或 P95 值
– 不要在开发机上测生产性能:CPU 负载、GC 频率、JVM 参数差异都会掩盖真实瓶颈
容易被忽略的一点:System.nanoTime() 的分辨率取决于底层 OS 和硬件,Linux 上通常是 1–15 纳秒,但 Windows 上可能低至 15ms(尤其老版本),此时测不到 sub-millisecond 级别的差异,得换更专业的工具如 async-profiler。

















