Java内存泄漏四大高发场景是静态集合类、未关闭资源、内部类持有外部类引用、ThreadLocal;典型表现有堆内存持续上升、Full GC无效、频繁OOM;可用jmap+jhat定位,优先用jmap -histo快速采样;WeakReference适用于防泄漏临时绑定,SoftReference适合可重建缓存;Spring Bean需显式清理资源,不能依赖GC或@PreDestroy。

Java中哪些对象最容易引发内存泄漏
静态集合类、未关闭的资源、内部类持有外部类引用、线程本地变量(ThreadLocal)是四大高发场景。比如把 ArrayList 声明为 static 并持续 add(),只要类不卸载,这个列表及其所有元素就永远无法被 GC 回收。
常见错误现象包括:堆内存使用量随时间持续上升、Full GC 频次增加但老年代回收效果差、OutOfMemoryError: Java heap space 反复出现且 dump 后发现大量业务对象堆积。
-
static Map缓存未设上限或未清理过期项 - 数据库连接、文件流、网络 socket 使用后未在
finally或 try-with-resources 中显式关闭 - 非静态内部类(如监听器、Runnable)被线程池长期持有,间接持有了外部 Activity/Service 实例(Android)或 Spring Bean
-
ThreadLocal在线程复用场景(如 Tomcat 线程池)中未调用remove(),导致旧请求数据残留
如何用 jmap + jhat 快速定位泄漏对象
不要等 OOM 才排查。先用 jps -l 查到目标进程 PID,再执行:
jmap -dump:format=b,file=heap.hprof
生成的 heap.hprof 可用 jhat 启动分析服务:
立即学习“Java免费学习笔记(深入)”;
jhat -port 7000 heap.hprof
然后浏览器打开 http://localhost:7000,重点看:References to the selected object 和 Heap histogram。如果发现某个业务类实例数异常多(比如 com.example.UserContext 占比超 40%),点进去看它的 GC Roots 路径——通常能直接看到是哪个静态字段或线程栈在强引用它。
注意:jmap -histo 更轻量,适合线上快速采样:
jmap -histo| head -20
重点关注 instances 列数值突增的类,尤其是你自己写的类。
WeakReference 和 SoftReference 的实际选用边界
不是所有缓存都适合用 WeakReference。它的回收时机由 GC 决定,且一旦被回收就不可恢复;而 SoftReference 会在内存不足时才回收,更适合做内存敏感型缓存。
- 用
WeakReference:临时上下文绑定(如将Activity引用传给异步任务时防内存泄漏)、监听器解注册辅助 - 用
SoftReference:图片缓存、模板解析结果缓存等可重建但代价较高的对象 - 不用
Reference子类:数据库连接、文件句柄、锁对象——这些必须显式释放,不能依赖 GC
示例:防止 Handler 持有 Activity 泄漏
static class SafeHandler extends Handler {
private final WeakReference mActivity;
SafeHandler(Activity activity) {
mActivity = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
Activity activity = mActivity.get();
if (activity != null && !activity.isFinishing()) {
// 安全使用
}
}
}
Spring Bean 生命周期中容易忽略的清理点
Spring 管理的单例 Bean 默认不会销毁,但其中持有的资源(如定时任务、监听器、线程池)必须手动清理。靠 @PreDestroy 不够,因为容器异常关闭时该方法可能不执行。
- 实现
DisposableBean接口并重写destroy(),比@PreDestroy更可靠(Spring 会保证调用) - 对
ScheduledExecutorService,必须调用shutdownNow()并awaitTermination(),否则 JVM 无法退出 - 使用
ApplicationRunner或CommandLineRunner注册 JVM 关闭钩子(Runtime.getRuntime().addShutdownHook())作为兜底
特别注意:Spring Boot Actuator 的 /actuator/shutdown 端点默认关闭,启用后也只触发容器关闭流程,不替代显式资源释放逻辑。
GC 不会帮你关数据库连接,也不会替你注销事件监听器。最可靠的清理永远发生在代码明确执行的那一刻,而不是等待某个“自动”机制。










