一文盘点Java中常见内存泄漏场景与解决方法
作者:烟沙九洲
今天我们来一起聊一聊有哪些情况会导致内存泄漏。
什么是 内存泄漏 呢
内存泄漏 是指对象 已经不再被程序使用,但因为某些原因 无法被垃圾回收器回收,长期占用内存,最终可能引发 OOM(OutOfMemoryError)。
接下来我们看一下常见的几类内存泄漏场景。
1、生命周期长的集合
将对象放入 静态 或 生命周期很长 的集合(如 public static List list = new ArrayList<>();),即使后面不再需要,集合仍持有其引用,导致无法GC。
2、未关闭的资源
连接、流等资源未调用 close() 方法关闭。这些资源不仅占用内存,还可能占用文件句柄(操作系统分配的唯一标识,凭它,你才能操作文件资源)、网络连接等系统资源。比如 数据库连接、文件流(FileInputStream)、Socket连接 等。
public class FileTest {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// 读取文件,未调用 fis.close()
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
// 未调用 fis.close() → fis 持有 Native 引用,无法回收
}
}
}
3、ThreadLocal 使用不当
将对象存入 ThreadLocal 后,未在后续调用 remove() 清理。若线程来自线程池(会复用),其 ThreadLocalMap 中的值会一直存活。
public class ThreadLocalTest {
private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 线程池(核心线程长期存活)
TThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("my-thread-pool-%d").setDaemon(false).setPriority(Thread.NORM_PRIORITY).build(),
new ThreadPoolExecutor.AbortPolicy()
);
executor.submit(() -> {
User user = new User("李四", 30);
userThreadLocal.set(user); // 存储到 ThreadLocal
// 业务执行完毕,未调用 remove()
// 核心线程不会销毁,ThreadLocal 仍持有 user 引用
});
}
}
ps:未进行 remove() ,还可能会导致 ThreadLocal 取值串门。
4、内部类与外部类引用
非静态内部类(或匿名类)会 隐式持有 外部类的引用。如果内部类实例生命周期更长(如被缓存或另一个线程引用),会阻止外部类被回收。
public class OuterClass {
private byte[] bigData = new byte[1024 * 1024 * 10]; // 10MB 大对象
// 非静态内部类
class InnerClass {
// 内部类隐式持有 OuterClass 引用
}
public InnerClass createInner() {
return new InnerClass();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
InnerClass inner = outer.createInner();
// 置空外部类引用,但 inner 仍持有 outer 引用
outer = null;
// 若 inner 被静态变量/线程长期持有 → outer 对象(含 bigData)无法回收
}
}
5、 监听器与回调
注册了 监听器 或 回调 后,在对象不再需要时 没有注销,导致源对象仍持有监听器的引用(比如 事件监听器、消息队列的消费者等)。
排查工具推荐
- MAT(Memory Analyzer Tool): 分析堆 Dump 文件,定位泄漏对象、引用链(谁在持有泄漏对象);
- VisualVM: JDK 自带工具,监控内存占用趋势,生成堆 Dump,简单排查泄漏。
6、静态集合类
如 ArrayList、HashMap 等等集合类在类中创建为静态变量时,那么他们的生命周期与程序是一致的,由于一些集合没有删除元素的特性或者并没有对其包含的对象进行处理,导致容器中对象的生命周期与容器一致,不能被释放回收,然而对象本已经不再使用,就造成了内存泄漏。这样看来,基本特征为长生命周期对象持有短生命周期对象的引用,尽管短生命周期对象已不再使用,但是因为长生命周期对象的引用使其不能被GC回收。
7、变量的作用域不合理
如本该唯一定义在某方法的变量定义在了全局变量。一般来讲,一个变量的作用范围大于其所被使用的范围,可能发生内存泄漏,表现在存在时间大于使用时间,即使用完了但是还不能被回收,就比如下面的列子。另外,如果没有及时的将未使用的对象置 null,也有可能导致内存泄漏。
package com.liqia.common.core;
/**
* 内存泄漏例子
*
* @author chenq
*/
public class DemoMemoryLeak {
/**
* 消息
*/
private String info;
public void receiveAndSaveInfo() {
// 模拟接受消息
receiveInfo();
// 模拟存储消息
saveInfo();
}
}
这里的变量 info 在方法 receiveAndSaveInfo 中进行赋值和保存,在该方法执行完毕后本应该被 GC 回收,但由于全局变量的生命周期是跟随对象的,所有当方法执行完不能被回收,可能造成内存泄漏。
8、内部类持有外部类
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
9、改变哈希值
当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄露。
10、栈引起的内存泄漏
这段模拟栈操作的代码存在隐蔽的内存泄漏问题。定位到pop()函数,在return语句中,当我们弹出一个元素时,只是简单的让栈顶指针(size)-1。逻辑上,栈中的这个元素已经弹出,已经没有用了。但是事实上,被弹出的元素依然存在于elements数组中,它依然被elements数组所引用,GC是无法回收被引用着的对象的。也许你期望等这整个栈失去引用(将被GC回收时),栈内的elements数组一起被GC回收。但是实际的使用过程中,又有谁能够预料到这个栈会存活多长时间。为了保险起见,我们需要在弹出一个元素的时候,就让这个元素失去引用,便于GC回收。我们只需要让Pop()函数弹出时,同时解除对弹出元素的引用即可。
package com.liqia.common.core;
import java.util.Arrays;
import java.util.EmptyStackException;
/**
* 内存泄漏例子
*
* @author chenq
*/
public class DemoMemoryLeak {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
public DemoMemoryLeak() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object o) {
ensureCapacity();
elements[size++] = o;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
// Object o = elements[--size];
// elements[size] = null;
// return o;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, size * 2 + 1);
}
}
}
11、缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘,对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
这里我只列了常见的几种情况,欢迎大家补充其他内存泄漏场景。
到此这篇关于一文盘点Java中常见内存泄漏场景与解决方法的文章就介绍到这了,更多相关Java内存泄漏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
