推荐用 System.currentTimeMillis() 获取日志时间戳,因其毫秒精度足够、线程安全、UTC 基础、零对象创建、性能比 Instant.now() 快 2–3 倍,且兼容所有 JDK 版本;需配合日志框架格式化,避免手动拼接字符串,并防范时钟回拨与统一时区配置。

直接用 System.currentTimeMillis() 获取日志时间戳没问题,但要真正“靠谱”,得注意几个关键点:精度够用、线程安全、时区中立、格式可控,还要避免常见误用。
为什么日志里推荐用 currentTimeMillis() 而不是 Instant
日志系统对性能极其敏感,每条日志都调用一次时间获取,开销必须最小化。System.currentTimeMillis() 是 JVM 原生方法,不创建对象、无 GC 压力、调用路径最短,实测比 Instant.now().toEpochMilli() 快 2–3 倍。日志时间戳只要毫秒级精度就完全够用(人类读不出微秒差异),没必要为语义清晰牺牲吞吐量。
- 它返回的是 UTC 毫秒值,天然规避本地时区干扰,日志集中分析时不会因服务器部署地不同而错乱
- 所有 JDK 版本都支持,老系统、中间件、Agent 插件兼容性零风险
- 配合日志框架(如 Logback、Log4j2)的
%d{HH:mm:ss.SSS}等格式化器,能稳定输出可读时间
避免在日志中直接拼接字符串时间
别这么写:
logger.info("user=" + userId + ", ts=" + new Date(System.currentTimeMillis()));这会强制每次日志都创建 Date 对象和格式化字符串,严重拖慢高并发日志写入。正确做法是让日志框架自己处理:
- 配置 logback.xml 中的
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern> - 框架内部会在异步日志线程中统一调用
System.currentTimeMillis()并格式化,复用缓冲、减少锁竞争 - 如果必须在日志消息里带时间戳(比如调试临时打点),也只记毫秒值:
logger.debug("start processing, ts={}", System.currentTimeMillis()),留到展示层再转
注意系统时钟跳变带来的日志时间倒流
System.currentTimeMillis() 依赖操作系统时钟,NTP 校准或手动调时可能导致时间回拨。后果是:日志里出现“20:24:15”后面跟着“20:24:12”,排序混乱,监控告警误判。
- 生产环境务必启用平滑时钟调整(如 Linux 的
ntpd -x或chronyd makestep -1.0 -0.128),避免突变 - 关键业务日志可加一层简单防回拨逻辑(适用于日志量不大但要求严格排序的场景):
private static final AtomicLong lastTimestamp = new AtomicLong(0);
public static long safeCurrentTimeMillis() {
long now = System.currentTimeMillis();
long last = lastTimestamp.get();
if (now < last) {
now = last + 1; // 微调,保证单调递增
}
lastTimestamp.set(now);
return now;
}毫秒值本身不带时区,但日志解析时需明确约定
虽然 System.currentTimeMillis() 返回的是 UTC 毫秒数,但日志文件里通常只存格式化后的本地时间字符串(如 2026-06-09 20:24:33.127)。这就要求:
- 所有服务统一设置 JVM 时区(
-Duser.timezone=Asia/Shanghai或UTC),避免同一集群日志时间显示不一致 - ELK / Loki 等日志平台摄入时,必须配置对应时区解析规则;若原始日志含 UTC 时间戳字段(如
"ts":1749471873127),应优先用该字段做时间提取,而非解析字符串 - 跨时区排查问题时,直接用毫秒值换算成各时区时间,比肉眼比对字符串更可靠

















