【Java面试高频】- Java内存泄露如何排查呢?

常的一个误解是是:认为Java的自动垃圾回收完全使他们免于担心内存管理,虽然垃圾收集器做的很好,但即使是最好的程序也完全有可能成为严重破坏内存泄露的牺牲品。

当不必要地维护不再需要的对象引用时,会发生内存泄露。

实际上有四类内存问题具有相似和重叠的特征,但原因和解决方案各不相同:

  • Performance(性能):通常与过多的对象创建和删除,垃圾收集的长时间延迟,过多的操作系统系统页面交换等相关联;
  • Resource constrains(资源约束):当可用内存很少或内存过于分散而无法分配大对象时,这通常与Java堆相关;
  • Java heap leaks:经典的内存泄露,Java对象在不释放的情况下不断创建。这通常是由潜在对象引用引起的;
  • Native memory leaks(本机内存泄露):与Java堆之外的任何不断增长的内存利用率相关联,例如由JNI代码,驱动程序甚至JVM分配。

(1)解密OutMemoryError

OOM是内存泄露的常见指示。实质上,当没有足够的空间来分配新对象时,会抛出错误。当垃圾收集器找不到必要的空间,并且堆不能进一步开展,会多次尝试。因此,会出现错误以及堆栈跟踪;

诊断OOM的第一步是确定错误的实际含义,下面分析一些可能的错误类型:

  • java.lang.OutOfMemoryError: Java heap space
  • java.lang.OutOfMemoryError: PermGen space
  • java.lang.OutOfMemoryError: Requested array size exceeds VM limit
  • java.lang.OutOfMemoryError: request bytes for . Out of swap space?
  • java.lang.OutOfMemoryError: (Native method)

1.1 Java heap space

此错误消息不一定意味着内存泄露。实际上,问题可能与配置问题一样简单。

可能并不是应用程序的错误,而是应用程序服务器依赖于默认的堆太小了,可以通过调整JVM的内存参数解决了这个问题。

1.2 PermGen space

此错误消息表明永久代已满,永久代是存储类和方法对象的堆的区域。如果应用程序加载了大量的类或者应用程序实例化了大量字符串,那么则可能需要使用-XX: MaxPermSize选项增加永久代的大小;

1.3 Requested array size exceeds VM limit

此错误表示应用程序尝试分配大于堆大小的数组。例如,如果应用程序尝试分配512MB的数组但最大堆大小 为256MB,则将抛出此错误信息的OOM。在大多数情况下,问题是配置问题或应用程序尝试分配海量数组时导致的错误;

1.4 Request bytes for Out of swap space?

此消息似乎是一个OOM。但是,当本机堆的分配失败并且本机堆可能将被耗进时,HotSpot VM会抛出此异常。消息中包括失败请求的大小(以字节为单位)以及内存请求的原因。

如果抛出此类型的OOM,则可能需要在操作系统上使用故障排除实用程序来进一步诊断问题。在某些情况下,问题甚至可能与应用程序无关。例如,您可能会在以下情况下看到此错误:

  • 操作系统配置的交换空间不足。
  • 系统上的另一个进程是消耗所有可用的内存资源。

由于本机泄漏,应用程序也可能失败(例如,如果某些应用程序或库代码不断分配内存但无法将其释放到操作系统)

1.5 Native method

如果您看到此错误消息并且堆栈跟踪的顶部框架是本机方法,则该本机方法遇到分配失败。此消息与上一个消息之间的区别在于,在JNI或本机方法中检测到Java内存分配失败,而不是在Java VM代码中检测到。

如果抛出此类型的OOM,您可能需要在操作系统上使用实用程序来进一步诊断问题。

1.6 Application Crash Without OOM

有时,应用程序可能会在从本机堆分配失败后很快崩溃。如果您运行的本机代码不检查内存分配函数返回的错误,则会发生这种情况。

例如,如果没有可用内存,malloc系统调用将返回NULL。如果未检查malloc的返回,则应用程序在尝试访问无效的内存位置时可能会崩溃。根据具体情况,可能很难定位此类问题。

(2) 解决步骤

  • a.识别症状

在许多情况下,Java进程最终抛出一个OOM运行时异常,这是一个明确的指示,表明你的内存资源已经耗尽。在这种情况下,需要区分正常的内存耗尽和泄露;

通常,如果Java应用程序请求的存储空间超过运行时堆提供的存储空间,则可能是由于涉及不佳导致的。

可以使用下述命令来查看进程中堆内存的使用情况:

jhsdb jmap --pid your-java-serverce-pi --heap

Java应用排查工具 java排查内存泄露_Java

这个命令只能反映当前堆内存情况,用来查看堆大小、代分配是否合理还行,更多的信息则无法很直观的看出来。

  • b.启用详细的垃圾收集,查看GC情况

使用命令查看进程 GC 情况:

jstat -gcutil your-java-service-pid 1000 100

Java应用排查工具 java排查内存泄露_java面试高频_02

这个命令可以查看进程启动以来 Young GC / Full GC 的次数及时间,并且会间隔很短展示最新数据,主要用于判断系统 GC 频率是否有问题,GC 时间是否过长影响系统正常运行等。

  • c.查看GC历史

要查看GC历史,需要打印GC日志,应用启动命令类似如下:

nohup java -XX:+PrintGCDetails \  -Xloggc:log/gc.log \  -XX:+HeapDumpOnOutOfMemoryError \  -XX:HeapDumpPath=log/dump.log \      -jar ${linkname} > nohup.out 2>&1 &

查看服务运行两三天后的GC日志,过滤出其中 Full GC 的信息

Java应用排查工具 java排查内存泄露_内存泄露处理_03

可以观察到,经过多次 Full GC,箭头右边的反映 GC 后老年代堆内存大小的数值在增加。这说明有老年对象一直没有被释放,某个对象对这些对象的引用一直维持着。这种趋势下去,OutOfMemory 错误的出现不可避免,完全实锤了该服务的内存泄漏问题。

  • d.查看堆实例对象分类

查看进程堆中当前实例数前 20 类排名:

jmap -histo:live your-java-service-pid | head -n 20

Java应用排查工具 java排查内存泄露_内存泄露处理_04

查看进程堆中当前实例数前 20 且为该项目包路径下类排名:

jmap -histo:live your-java-service-pid | grep your-project-package-typical-word | head -n 20