Java内存泄漏与对象清理处理方法

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 启动分析服务:

jhat -port 7000 heap.hprof

然后浏览器打开 http://localhost:7000,重点看:References to the selected objectHeap 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 无法退出
  • 使用 ApplicationRunnerCommandLineRunner 注册 JVM 关闭钩子(Runtime.getRuntime().addShutdownHook())作为兜底

特别注意:Spring Boot Actuator 的 /actuator/shutdown 端点默认关闭,启用后也只触发容器关闭流程,不替代显式资源释放逻辑。

GC 不会帮你关数据库连接,也不会替你注销事件监听器。最可靠的清理永远发生在代码明确执行的那一刻,而不是等待某个“自动”机制。