
Timer 和 TimerTask 为什么容易漏掉 cancel() 导致内存泄漏
Java 的 Timer 是单线程调度器,所有 TimerTask 都在同一个后台线程执行;一旦任务没显式取消,Timer 实例会一直持有对任务的强引用,哪怕外部对象已不可达。JVM 不会回收它,后台线程持续运行,形成隐式内存泄漏。
常见错误现象:OutOfMemoryError: unable to create new native thread,尤其在频繁创建新 Timer 的 Web 应用中(比如每次 HTTP 请求都 new 一个)。
- 必须在任务完成或不再需要时调用
timer.cancel(),且建议紧接着置timer = null -
timer.purge()只清理已取消但未执行的任务队列,不能替代cancel() - 若任务本身是匿名内部类或 Lambda,还可能隐式持有所在类的引用——别让定时器成为 Activity/Servlet 的“吊命线”
schedule() vs scheduleAtFixedRate() 的执行逻辑差异
两者都接受延迟和周期参数,但对“执行偏差”的处理完全不同:前者按“上一次实际结束时间 + 周期”计算下次触发点,后者严格按“上一次计划开始时间 + 周期”推进,不考虑执行耗时。
使用场景举例:做心跳上报适合 scheduleAtFixedRate()(保证服务端看到等间隔信号);而日志轮转适合 schedule()(避免前次压缩未完成就启动下一轮)。
立即学习“Java免费学习笔记(深入)”;
- 如果某次任务执行超时,
scheduleAtFixedRate()可能连续触发多次(“追赶模式”),甚至堆积线程阻塞 -
schedule()则会跳过错过的执行点,只保障“不早于计划时间”,更保守 - 两个方法都不支持任务失败重试、动态调整周期等高级行为——真有这类需求,直接换
ScheduledThreadPoolExecutor
TimerTask 的 run() 方法里抛异常会导致整个 Timer 停摆
Timer 后台线程遇到未捕获异常会直接终止,后续所有已调度但未执行的任务全部丢失,且不会报错日志(除非你重写了 Thread.setDefaultUncaughtExceptionHandler)。
典型错误现象:定时任务某天突然不跑了,查日志没有任何异常记录,只有 JVM 进程里少了一个 Timer-0 线程。
- 务必在
run()方法最外层加try-catch(Throwable),至少打日志 - 不要依赖
finally做关键清理——线程一崩,finally就不执行了 - 若需保证任务失败后仍继续调度,得自己在 catch 里重新
timer.schedule(...),但注意别造成重复调度
Android 或 Servlet 环境下直接用 Timer 很危险
Android 的 Activity 销毁、Servlet 的 Context 关闭,都不自动清理你手动创建的 Timer;后台线程继续跑,试图更新已销毁的 UI 或访问已关闭的数据库连接,结果就是 NullPointerException 或 IllegalStateException。
兼容性影响:从 Java 5 开始,Timer 就被标记为“仅适用于简单场景”,官方文档明确建议生产环境优先用 ScheduledThreadPoolExecutor。
- Android 中应改用
Handler.postDelayed()(主线程)或Executors.newScheduledThreadPool()(后台) - Web 应用推荐用 Quartz 或 Spring 的
@Scheduled,它们内置生命周期管理 - 哪怕只是写个本地工具脚本,也建议用
java.util.concurrent.ScheduledExecutorService—— 它的shutdown()更可控,支持多线程并发执行任务
Timer 的设计初衷就是轻量、简单、一次性;把它当长期驻留的调度中枢用,等于把自行车当卡车开——不是跑不动,是出事前你根本不知道哪根辐条快断了。










