Java的内存管理机制依赖垃圾回收器(GC)自动回收不再使用的对象内存,但即使有GC存在,依然可能出现内存泄漏问题,也就是对象已经不再被程序使用,却无法被GC回收,导致内存被持续占用最终可能引发内存溢出。

什么是Java内存泄漏
Java内存泄漏指的是,程序中某些对象已经不再被业务逻辑使用,但是由于存在被其他存活对象引用的关系,导致GC无法回收这些对象的内存,随着程序运行,这类无法回收的对象不断堆积,最终会占用大量堆内存,甚至触发OutOfMemoryError错误。
和C++等需要手动释放内存的语言不同,Java的内存泄漏不是开发者忘记调用释放方法,而是引用关系管理不当导致的。比如下面的简单示例,就能体现这种引用导致无法回收的情况:
import java.util.ArrayList;
import java.util.List;
public class SimpleLeakDemo {
// 静态集合,生命周期和类一致,属于长生命周期对象
private static final List<Object> LEAK_LIST = new ArrayList<>();
public void addObject() {
Object obj = new Object();
// 将短生命周期的obj放入长生命周期的静态集合,obj的引用被一直持有
LEAK_LIST.add(obj);
// 即使方法执行结束,obj也不会被GC回收
}
public static void main(String[] args) {
SimpleLeakDemo demo = new SimpleLeakDemo();
for (int i = 0; i < 10000; i++) {
demo.addObject();
}
// 此时LEAK_LIST中持有大量Object对象的引用,这些对象都无法被回收
}
}
Java资源无法释放的常见原因
1. 长生命周期对象持有短生命周期对象引用
这是最常见的内存泄漏原因,比如静态集合、单例对象中持有普通业务对象的引用。单例对象的生命周期和应用程序一致,如果单例中缓存了大量临时对象且没有清理机制,这些临时对象就会一直被引用无法回收。上面的示例代码就是典型的静态集合持有短生命周期对象的情况。
2. 未正确关闭资源流
Java中的IO流、数据库连接、Socket连接等资源,在使用后需要手动调用关闭方法,或者放在try-with-resources语句中自动关闭。如果没有正确关闭,这些资源对象会一直占用内存,同时可能关联的操作系统资源也无法释放。比如下面的文件流未关闭的示例:
import java.io.FileInputStream;
import java.io.IOException;
public class StreamLeakDemo {
public void readFile(String filePath) {
FileInputStream fis = null;
try {
fis = new FileInputStream(filePath);
// 读取文件逻辑
} catch (IOException e) {
e.printStackTrace();
}
// 没有调用fis.close(),文件流资源无法释放
}
}
正确的做法应该是使用try-with-resources,或者手动在finally块中关闭:
import java.io.FileInputStream;
import java.io.IOException;
public class CorrectStreamDemo {
public void readFile(String filePath) {
// try-with-resources会自动关闭实现AutoCloseable接口的资源
try (FileInputStream fis = new FileInputStream(filePath)) {
// 读取文件逻辑
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 缓存使用不当
很多开发者会使用缓存来提升程序性能,但是如果缓存没有设置过期策略或者淘汰机制,缓存中的对象会一直被引用。比如使用HashMap做缓存,不断往里面添加数据却从不删除,随着时间推移缓存占用的内存会越来越大。这种情况可以使用WeakHashMap,它的键是弱引用,当键没有其他强引用时,对应的键值对会被自动回收。
4. 监听器和其他回调未注销
如果向某个事件源注册了监听器或者回调方法,却没有在不需要的时候注销,那么事件源会一直持有监听器对象的引用,导致监听器对象无法被回收。比如AWT或者Swing中的事件监听器,如果组件销毁时没有注销监听器,就会出现这类问题。
5. 内部类持有外部类引用
Java的非静态内部类会隐式持有外部类的引用,如果内部类的生命周期比外部类长,就会导致外部类无法被回收。比如在一个方法中创建非静态内部类的实例,并且把这个实例传递到方法外部,那么即使外部类的实例已经不再使用,也会因为内部类持有其引用而无法被GC回收。这种情况可以将内部类改为静态内部类,避免持有外部类的引用。
如何排查和规避内存泄漏
排查Java内存泄漏可以使用JVM自带的工具,比如jmap导出堆内存快照,再用jhat或者MAT工具分析快照,找到占用内存最多的对象以及它们的引用链,就能定位到泄漏的原因。日常开发中规避内存泄漏需要注意几点:及时清理集合中不再使用的对象,资源流使用try-with-resources管理,缓存设置合理的淘汰策略,不再使用的监听器及时注销,合理使用弱引用、软引用等引用类型。