Java 中常见的内存泄漏场景
内存泄漏是软件开发中一个严重的缺陷,会随着时间的推移导致应用程序崩溃或性能下降。以下是 Java 中最常见的内存泄漏场景:
1. 持有对外部对象的引用
当一个对象持有对外部对象的引用时,JVM 无法在外部对象不用时对其进行垃圾回收。例如:
class Outer {
private Inner inner;
public Outer() {
inner = new Inner(); // 持有对 Inner 的引用
}
}
class Inner {
// ...
}
2. 静态引用
静态变量存储在 JVM 中的永久内存中,它们永远不会被垃圾回收。如果静态变量持有对对象的引用,則该对象无法被垃圾回收。例如:
public class Example {
private static List<Object> objects = new ArrayList<>();
public static void main(String[] args) {
objects.add(new Object());
}
}
3. 无效的监听器
当一个侦听器不再使用但仍附加到一个事件源时,会发生内存泄漏。例如:
import javax.swing.*;
public class ListenerLeak {
private JButton button;
public ListenerLeak() {
button = new JButton();
button.addActionListener(e -> {
// ...
});
}
// 忘记从按钮中移除监听器
}
4. 线程局部变量
线程局部变量存储在每个线程的线程局部存储 (TLS) 中,并且只要线程处于活动状态,它们就不会被垃圾回收。如果您在已完成的线程中使用了线程局部变量,可能会导致内存泄漏。例如:
public class ThreadLocalLeak {
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
threadLocal.set(new Object()); // 在线程中设置值
});
thread.start();
thread.interrupt(); // 中断线程
// 线程局部存储未得到清理
}
}
5. 循环引用
当两个或多个对象相互引用时,会发生循环引用。这会导致 JVM 无法识别它们已经不再使用,从而导致内存泄漏。例如:
public class CycleReference {
private CycleReference other;
public CycleReference() {
other = new CycleReference();
other.other = this; // 循环引用
}
}
实战案例
应用服务器中的内存泄漏
以下是应用服务器中典型的内存泄漏场景:
- 线程保存对 servlet 对象的引用,即使该 servlet 已完成。
- 静态持有器 (如数据库连接池) 保持对持久连接的引用,即使这些连接不再需要。
- 侦听器未从组件中移除,导致侦听器的引用被保留。
通过了解常见的内存泄漏场景并采用适当的编码实践,可以降低 Java 应用程序中内存泄漏的风险,以确保其稳定性和性能。