跳到主要内容

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类调用。此时可以着手优化问题代码。

内存泄露处理方法总结

  1. 使用工具或命令监控Java程序,确认是否存在内存泄漏。

  2. 使用jmap保存程序内存快照,并用工具分析。

  3. 通过分析大对象和引用树,定位未清理的对象及其引用代码。

  4. 针对问题代码,决定优化代码或增加内存空间。

如何防止内存泄漏

在Java中,预防内存泄漏的关键在于及时释放不再需要的对象引用,合理设计代码,并管理资源。以下是一些有效的策略:

  1. 及时释放不再需要的对象引用

避免长时间保留对象的引用,特别是当对象不再需要时,将它们的引用设置为null,让垃圾回收器能够尽快回收它们。

  1. 慎用静态集合和单例

静态集合(如ArrayList、HashMap等)和单例模式会持有对象的引用,直到JVM退出,容易导致内存泄露。将不再使用的对象及时从集合中移除,避免长时间引用。

  1. 使用弱引用和软引用

Java提供了WeakReferenceSoftReference类,可用于存储非强引用对象,帮助垃圾回收器识别可以被回收的对象。

  • WeakReference:弱引用对象在下次垃圾回收时会被回收,适合缓存中短暂的数据。
  • SoftReference:软引用对象会在内存不足时被回收,适合用于较大的缓存。
  1. 及时关闭资源

使用完资源后(如数据库连接、文件、网络连接等),应立即关闭,防止内存泄露。可以使用try-with-resources语句确保资源被关闭。

try (FileInputStream fis = new FileInputStream("example.txt")) {
// 使用资源
} catch (IOException e) {
e.printStackTrace();
} // 自动关闭资源
  1. 确保线程能正确终止

创建的线程未能正确终止可能导致内存泄露。特别是自定义线程和线程池使用时,确保线程在任务结束后能正确退出。

使用ExecutorService管理线程池,并在不需要时调用shutdown()方法结束线程池。