Java内存泄露分析技巧
什么是内存泄露?
在Java程序运行时,系统会为其分配一块固定的内存空间,用于存储运行时的临时数据。如果程序试图使用超出可用内存范围的资源,就可能导致系统无法为新对象分配内存,从而抛出OutOfMemoryError
异常,这种情况通常称为内存溢出(Out of Memory, OOM)。

Java具备自动内存管理机制,即垃圾回收器(GC),负责回收不再被引用或使用的对象所占用的内存空间,从而降低内存溢出风险。如下图所示,每当内存使用量达到一定程度时,GC的活动会增加,以释放空间。
然而,有时即使对象不再需要,仍然被引用着,导致GC
无法释放这些内存资源。随着时间推移,这些未释放的对象会不断累积,可能最终导致内存泄漏,引发OutOfMemoryError
或显著降低程序性能。
常见内存泄漏现象包括:
- 从数据库一次性获取大量数据,导致内存被大量占用。
- 实体类或集合中的引用在使用后未清理,阻碍JVM回收。
- 静态变量中存储了大量数据,持续占用内存空间。
如何处理内存泄漏
当怀疑存在内存泄漏时,可使用性能分析工具(如VisualVM、JProfiler)来监控程序的内存使用情况。这些工具的核心功能通常相似。
监控并确认内存泄漏
首先,观察JVM内存使用情况,以确认是否确实存在内存泄漏。正常情况下,内存回收后未被释放的空间较小,每次回收后剩余的内存量基本相同,如下图所示:

如果发现每次GC后遗留的未释放内存逐渐增多,很可能是出现了内存泄漏:

分析泄漏原因
若分析本地程序,直接用性能分析工具连接到运行的JVM即可。如需排查服务器上的Java程序,建议使用jmap
生成heap dump
文件来分析堆快照。
问题代码示例
下面是一段会导致内存泄漏的示例代码。TempServiceImpl
类中的appendData
方法会不断向LARGE_DATA
变量中追加数据:
public class TempServiceImpl {
public static String LARGE_DATA = "";
public void appendData() {
for (;;) {
doAppend();
}
}
public void doAppend() {
LARGE_DATA = LARGE_DATA + "模拟大量重复的数据";
}
}
接着,在/test/oom
接口中调用此方法:
@Controller
public class IndexController {
@GetMapping("/test/oom")
public void testOOM() {
TempServiceImpl tempServiceImpl = new TempServiceImpl();
tempServiceImpl.appendData();
}
}
使用工具分析内存快照
通过生成内存快照并在工具中打开,类
选项卡展示了拍摄快照时内存中所有类的数量和大小。在这里可以看到75840个char[]对象占用14M的空间。
分析char[]对象时,可以右键选中后选择“使用选定对象”,观察对象的分配树、最大对象、引用和传出引用等信息。


以下是几个重要选项:
- 最大对象:显示按保留大小排序的对象,便于发现占用内存较大的对象。
- 引用:显示对象的引用树,帮助快速定位泄漏代码。
- 传出引用:展示当前对象集的引用对象树(字段),用于查看对象的内存空间分配情况。
- 传入引用:展示引用当前对象的引用树,常用于追踪问题代码来源。
下图是char[]对象的传入引用树:
从图中可以看出,大对象源于TempServiceImpl
类中的静态变量LARGE_DATA
,该类被IndexController
类调用。此时可以着手优化问题代码。
内存泄露处理方法总结
-
使用工具或命令监控Java程序,确认是否存在内存泄漏。
-
使用jmap保存程序内存快照,并用工具分析。
-
通过分析大对象和引用树,定位未清理的对象及其引用代码。
-
针对问题代码,决定优化代码或增加内存空间。
如何防止内存泄漏
在Java中,预防内存泄漏的关键在于及时释放不再需要的对象引用,合理设计代码,并管理资源。以下是一些有效的策略:
- 及时释放不再需要的对象引用
避免长时间保留对象的引用,特别是当对象不再需要时,将它们的引用设置为null,让垃圾回收器能够尽快回收它们。
- 慎用静态集合和单例
静态集合(如ArrayList、HashMap等)和单例模式会持有对象的引用,直到JVM退出,容易导致内存泄露。将不再使用的对象及时从集合中移除,避免长时间引用。
- 使用弱引用和软引用
Java提供了WeakReference
和SoftReference
类,可用于存储非强引用对象,帮助垃圾回收器识别可以被回收的对象。
WeakReference
:弱引用对象在下次垃圾回收时会被回收,适合缓存中短暂的数据。SoftReference
:软引用对象会在内存不足时被回收,适合用于较大的缓存。
- 及时关闭资源
使用完资源后(如数据库连接、文件、网络连接等),应立即关闭,防止内存泄露。可以使用try-with-resources语句确保资源被关闭。
try (FileInputStream fis = new FileInputStream("example.txt")) {
// 使用资源
} catch (IOException e) {
e.printStackTrace();
} // 自动关闭资源
- 确保线程能正确终止
创建的线程未能正确终止可能导致内存泄露。特别是自定义线程和线程池使用时,确保线程在任务结束后能正确退出。
使用ExecutorService管理线程池,并在不需要时调用shutdown()方法结束线程池。