内存泄漏是一个普遍存在于许多应用程序中的问题,它会导致程序随着时间的推移运行变慢并最终可能因资源耗尽而崩溃。以下是一些常见的内存泄漏原因:
1. 静态集合类导致的内存泄漏
静态集合的生命周期与 JVM 一致,因此其中的对象不会被自动回收。如果没有适当清理这些集合,就可能导致内存泄漏。
1 | public class OomTest { |
解决方案
- 使用弱引用:
WeakHashMap
或WeakReference
可以减少内存泄漏的风险。 - 显式清理集合:在不需要对象时,及时调用
list.clear()
或remove()
方法。 - 避免不必要的静态变量:能用局部变量的场景尽量不要用静态变量。
2. 单例模式导致的内存泄漏
单例对象的生命周期贯穿整个 JVM 运行周期,因此如果它持有外部对象的引用,那么该对象也不会被回收。
1 | public class Singleton { |
解决方案
- 避免持有外部对象的强引用,改用
WeakReference
或SoftReference
。 - 提供
clear()
方法 以显式释放引用。 - 使用依赖注入(DI)管理单例生命周期,避免长期持有不必要的资源。
3. 变量作用域过大
变量的作用域应尽可能小,以便它们尽早成为垃圾回收(GC)的候选对象。如果一个对象的引用在方法执行结束后仍然存在,可能导致内存泄漏。
1 | public class Simple { |
解决方案
-
尽量使用局部变量,而不是成员变量:
1
2
3
4public void method1() {
Object tempObject = new Object();
// tempObject 作用域仅限于 method1(),方法结束后即可被 GC 回收
} -
手动置空:在不再需要对象时,将其设为
null
(适用于全局变量或大对象)。1
object = null;
4. 资源未正确关闭(数据库连接、IO、Socket 等)
数据库连接、文件流、网络连接等资源如果未正确关闭,会导致内存泄漏,并可能导致系统资源耗尽。
问题示例
1 | try { |
解决方案
-
使用
try-with-resources
(JDK 7+):1
2
3
4
5
6
7try (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
5finally {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
}
5. ThreadLocal 使用不当
ThreadLocal
变量存储在线程的 ThreadLocalMap
中,线程池中的线程会被重用,因此如果 ThreadLocal
变量没有手动清除,会导致内存泄漏。
问题示例
1 | private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>(); |
解决方案
-
手动调用
remove()
:1
2
3
4
5
6try {
threadLocal.set(new byte[1024 * 1024]);
// 业务逻辑
} finally {
threadLocal.remove();
} -
使用
InheritableThreadLocal
需要格外小心,因为它的值会被子线程继承,可能加剧内存泄漏问题。
6. HashMap 键的 hashCode
变化导致内存泄漏
当一个对象被用作 HashMap
的键时,如果在放入 Map
之后修改了影响 hashCode()
计算的字段,这可能导致无法删除该键,导致内存泄漏。
问题示例
1 | class Key { |
解决方案
-
确保
hashCode()
依赖的字段是不可变的,例如使用final
:1
2
3
4class Key {
private final String id;
...
} -
使用
ImmutableMap
(Guava)或Collections.unmodifiableMap()
,防止修改键值。
总结
为了提高程序的健壮性,减少内存泄露,可以进行以下优化:
内存泄漏原因 | 解决方案 |
---|---|
静态集合类 | 避免使用静态集合存储长生命周期对象,使用 WeakReference |
单例模式 | 避免持有外部对象的强引用,提供 clear() 方法 |
变量作用域过大 | 变量作用域尽可能小,方法结束后及时释放 |
资源未关闭 | 使用 try-with-resources 或 finally 关闭资源 |
ThreadLocal 使用不当 | 手动调用 remove() 释放变量 |
HashMap 键的 hashCode 变化 |
使用不可变对象作为 Map 键 |