说说常见的发生内存泄露的原因

帅旋
帅旋
关注
充电
发布于 2024-04-21 | 更新于 2025-02-15

内存泄漏是一个普遍存在于许多应用程序中的问题,它会导致程序随着时间的推移运行变慢并最终可能因资源耗尽而崩溃。以下是一些常见的内存泄漏原因:

1. 静态集合类导致的内存泄漏

静态集合的生命周期与 JVM 一致,因此其中的对象不会被自动回收。如果没有适当清理这些集合,就可能导致内存泄漏。

1
2
3
4
5
6
7
8
public class OomTest {
static List<Object> list = new ArrayList<>();

public void addElement() {
Object tempObj = new Object();
list.add(tempObj); // tempObj 永远不会被 GC 回收
}
}

解决方案

  • 使用弱引用WeakHashMapWeakReference 可以减少内存泄漏的风险。
  • 显式清理集合:在不需要对象时,及时调用 list.clear()remove() 方法。
  • 避免不必要的静态变量:能用局部变量的场景尽量不要用静态变量。

2. 单例模式导致的内存泄漏

单例对象的生命周期贯穿整个 JVM 运行周期,因此如果它持有外部对象的引用,那么该对象也不会被回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Object cachedData; // 长生命周期对象持有短生命周期对象引用

private Singleton() {}

public static Singleton getInstance() {
return INSTANCE;
}

public void setCachedData(Object data) {
this.cachedData = data;
}
}

解决方案

  • 避免持有外部对象的强引用,改用 WeakReferenceSoftReference
  • 提供 clear() 方法 以显式释放引用。
  • 使用依赖注入(DI)管理单例生命周期,避免长期持有不必要的资源。

3. 变量作用域过大

变量的作用域应尽可能小,以便它们尽早成为垃圾回收(GC)的候选对象。如果一个对象的引用在方法执行结束后仍然存在,可能导致内存泄漏。

1
2
3
4
5
6
7
8
9
public class Simple {
Object object;

public void method1() {
object = new Object();
// 由于 object 是成员变量,它的生命周期与对象一致
// 该对象在方法执行完后仍然存在,可能导致不必要的内存占用
}
}

解决方案

  • 尽量使用局部变量,而不是成员变量:

    1
    2
    3
    4
    public void method1() {
    Object tempObject = new Object();
    // tempObject 作用域仅限于 method1(),方法结束后即可被 GC 回收
    }
  • 手动置空:在不再需要对象时,将其设为 null(适用于全局变量或大对象)。

    1
    object = null;

4. 资源未正确关闭(数据库连接、IO、Socket 等)

数据库连接、文件流、网络连接等资源如果未正确关闭,会导致内存泄漏,并可能导致系统资源耗尽。

问题示例

1
2
3
4
5
6
7
8
9
10
try {
Connection conn = DriverManager.getConnection("url", "user", "pass");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM table");
// 这里如果发生异常,资源可能不会被释放
} finally {
rs.close();
stmt.close();
conn.close();
}

解决方案

  • 使用 try-with-resources(JDK 7+):

    1
    2
    3
    4
    5
    6
    7
    try (Connection conn = DriverManager.getConnection("url", "user", "pass");
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT * FROM table")) {
    // 资源会自动关闭
    } catch (SQLException e) {
    e.printStackTrace();
    }
  • finally 中关闭资源

    (适用于 JDK 7 以下):

    1
    2
    3
    4
    5
    finally {
    if (rs != null) rs.close();
    if (stmt != null) stmt.close();
    if (conn != null) conn.close();
    }

5. ThreadLocal 使用不当

ThreadLocal 变量存储在线程的 ThreadLocalMap 中,线程池中的线程会被重用,因此如果 ThreadLocal 变量没有手动清除,会导致内存泄漏。

问题示例

1
2
3
4
5
6
private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

public void process() {
threadLocal.set(new byte[1024 * 1024]); // 1MB
// 这里忘记调用 remove(),线程池重用线程后,内存不会释放
}

解决方案

  • 手动调用 remove()

    1
    2
    3
    4
    5
    6
    try {
    threadLocal.set(new byte[1024 * 1024]);
    // 业务逻辑
    } finally {
    threadLocal.remove();
    }
  • 使用 InheritableThreadLocal 需要格外小心,因为它的值会被子线程继承,可能加剧内存泄漏问题。


6. HashMap 键的 hashCode 变化导致内存泄漏

当一个对象被用作 HashMap 的键时,如果在放入 Map 之后修改了影响 hashCode() 计算的字段,这可能导致无法删除该键,导致内存泄漏。

问题示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Key {
private String id;

public Key(String id) {
this.id = id;
}

@Override
public int hashCode() {
return id.hashCode();
}

@Override
public boolean equals(Object obj) {
return obj instanceof Key && ((Key) obj).id.equals(this.id);
}
}

public class HashMapLeak {
public static void main(String[] args) {
Map<Key, String> map = new HashMap<>();
Key key = new Key("123");
map.put(key, "value");

// 修改 key 导致 hashCode 变化
key.id = "456";

// 无法正确删除
map.remove(key);
System.out.println(map.size()); // 仍然是 1
}
}

解决方案

  • 确保 hashCode() 依赖的字段是不可变的,例如使用final

    1
    2
    3
    4
    class Key {
    private final String id;
    ...
    }
  • 使用 ImmutableMap(Guava)或 Collections.unmodifiableMap(),防止修改键值。


总结

为了提高程序的健壮性,减少内存泄露,可以进行以下优化:

内存泄漏原因 解决方案
静态集合类 避免使用静态集合存储长生命周期对象,使用 WeakReference
单例模式 避免持有外部对象的强引用,提供 clear() 方法
变量作用域过大 变量作用域尽可能小,方法结束后及时释放
资源未关闭 使用 try-with-resourcesfinally 关闭资源
ThreadLocal 使用不当 手动调用 remove() 释放变量
HashMap 键的 hashCode 变化 使用不可变对象作为 Map

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/faqs/jvm/causes-of-memory-leak.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。

请帅旋喝一杯咖啡

咖啡=电量,给帅旋充杯咖啡,他会满电写代码!

IT宅

关注公众号及时获取网站内容更新。