java内存泄漏主因是对象被静态集合、threadlocal、监听器或内部类意外长期持有。需清理静态集合、finally中remove threadlocal、注销监听器、用static或弱引用避免隐式引用。

Java内存泄漏通常不是因为忘了调用delete(Java没有这个操作),而是某些对象被意外地长期持有,导致垃圾回收器无法回收它们。最常见的情况是:本该短期存在的对象,被静态集合、缓存、监听器或线程局部变量等“悄悄留住”了。
检查静态集合类是否在持续添加却不清理
静态的Map、List、Set是内存泄漏高发区——它们生命周期与类相同,只要JVM不退出就一直存在。如果业务逻辑往里面不断加对象(比如用户会话、临时任务、日志上下文),又没配合理的淘汰策略,内存就会只增不减。
建议:
- 确认所有静态集合是否有明确的清理入口(如定时清理、按条件移除、LRU淘汰)
- 避免直接用
static Map<string object></string>做缓存;优先使用ConcurrentHashMap配合弱引用键(WeakHashMap)或Caffeine等带过期机制的库 - 用IDE的“Find Usages”功能快速定位静态集合的全部写入点,逐个评估是否真需长期持有
留意ThreadLocal未及时remove导致的泄漏
ThreadLocal本身不泄漏,但它的值如果被线程池中的线程反复复用,而你只set()不remove(),那么上一次请求存进去的大对象(如数据库连接、上下文Map)就会一直挂在该线程上,拖垮整个线程池的内存。
建议:
- 每次使用完
ThreadLocal后,在finally块中调用remove(),不要依赖set(null) - 如果是Web应用,可在Filter或拦截器的
doFilter()末尾统一清理常用ThreadLocal变量 - 注意子线程不会自动继承父线程的
ThreadLocal值,若需传递,要用InheritableThreadLocal并同样做好清理
排查未注销的监听器和回调引用
GUI组件、事件总线(如Guava EventBus)、RxJava订阅、Spring的ApplicationListener等,一旦注册监听器却忘记反注册,就会让被监听对象(通常是Activity、Service、Controller)无法被回收,尤其在Android或长生命周期服务中极易引发泄漏。
建议:
- 注册监听时保存返回的注册句柄(如
EventBus.register(this)对应unregister(this)),并在对象销毁前显式释放 - 使用弱引用监听器模式(如
WeakReference<listener></listener>)降低强引用风险 - 借助工具如
LeakCanary(Android)或MAT(Eclipse Memory Analyzer)分析堆转储,重点关注java.lang.ref.Finalizer队列和GC Roots路径
警惕内部类和匿名类隐式持有的外部类引用
非静态内部类、匿名类(如new Runnable(){...})默认持有一个指向外部类实例的隐式引用。如果这个内部类对象被长期持有(比如传给线程池、放入静态队列、作为回调缓存),那它所处的外部类(比如一个Activity、一个大Service)也会跟着“赖着不走”。
建议:
- 若内部类不需要访问外部类成员,就声明为
static,切断隐式引用 - 若必须访问,考虑用
WeakReference包装外部类引用,并在使用前判空 - 避免在构造函数或初始化块中把
this或匿名内部类实例暴露给其他长期存活对象
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











