堆内存泄漏处理流程

    最近项目的服务出现问题,查看grafana监控发现,服务挂掉前内存使用率极高,CPU使用率很低,推测是内存泄漏导致的。内存泄漏绝大多数是由堆内存泄漏导致的,所以直接使用JProfiler分析堆内存,最终查找到问题。这里分享一下,堆内存泄漏的处理流程。

0、理论基础

内存泄漏:

    是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。较少次数的内存泄漏一般不会造成太过严重的后果,但是如果内存泄漏不断累积,可用内存早晚会被耗尽。

内存溢出:

    是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。

不可回收的资源:

    JVM在垃圾回收的时候,是通过可达性分析算法来判断对象是否可以被回收。简单来讲,通常被待执行的方法中的属性引用的对象、被类变量引用的对象、以及被常量(被final修饰的属性)引用的对象,一般是不会被回收的。

    可达性分析算法详情参考:可达性分析算法

1、备份内存信息

    首先dump JVM的内存信息,这个信息用于后续的问题分析。如果重启了服务,JVM也会重启,这个信息就会丢失,所以务必先备份JVM的内存信息!!!

    总的来说,是通过jmap命令来备份JVM的内存信息,同时,需要我们了解进程的pid。

1、获取进程的pid:

ps -e | grep "NAME"

    其中,NAME指的是进程的名字。执行命令得到的信息中,包含pid,可以根据进程的信息找到对应的pid。

    当然,如果觉得查询出现的内容太多,也可以指定显示哪一列。例如,我的进程名字是“dialogue”,上面查询显示的第二列信息是pid,所以我的命令可以如下输入:

ps -e | grep "dialogue" | awk '{print $2}'

2、备份内存信息:

jmap -dump:format=b,file=[fileName] [pid]

    jmap命令可以用来获取JVM堆内存的信息,jmap -dump用来dump堆内存的信息。fileName指的是备份的堆内存信息保存的文件名,pid表示需要备份的进行的pid。例如,我的进程pid是73452,想要将堆内存信息保存在当前目录的jvm_heap.dump文件中,可以执行以下命令:

jmap -dump:format=b,file=jvm_heap.dump 73452

2、重启服务

    如果这是上线的服务,那么以最快速度恢复业务肯定是第一优先级的目标,所以在完成内存信息备份后需要立刻重启服务。当然,如果这是个完全不重要的服务,例如不重要的测试项目,这一点可以酌情考虑。

3、分析内存dump

    常用的分析JVM内存的工具有jhat、jstack、eclipse Memory Analyzer插件、以及JProfiler。JProfiler是功能非常强大,但是收费,新用户免费试用10天。

    这里介绍一下jhat。jhat是java自带的内存分析工具,可以将内存信息解析为html格式。命令格式:

jhat [Option] [fileName]

    例如,我想要分析的内存信息的文件在当前目录下,名字叫dump_test.dump,又不希望因为dump太大导致堆内存空间不足,可以通过如下命令启动:

jhat -J-Xmx1024m dump_test.dump

    执行命令后,会有类似如下的提示信息。如果提示“Server is ready”说明启动成功了:

Reading from dump_test.dump...
Dump file created Thu Jan 17 19:59:33 CST 2019
Snapshot read, resolving...
Resolving 195865 objects...
Chasing references, expect 39 dots.......................................
Eliminating duplicate references.......................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

    根据提示我们可以指定,HTTP Server运行在7000端口。在浏览器输入localhost:7000,可以看到结果。拉到界面最底部,有如果功能:

堆内存泄漏处理流程_第1张图片

    可以使用“Show heap histogram”这个功能,这是堆内存信息直方图,里面可以看到类、类的实例数量、类使用的内存大小的信息。我们可以给类按占用堆内存的大小排序,从大到小逐个分析,各个类的占用的堆内存大小是否合理,直到找到可能产生内存泄漏的类。

4、分析代码

问题定位:

    在上一步中,我们找到了可能的造成内存泄漏的类,所以现在我们需要去分析代码,找到问题的根源。

    在上一步找到的类中,发现了如下代码:

private static LinkedList serviceUrls = new LinkedList();

    这个类用来存放内存中被加载的ServiceUrl实例,每个实例生成的时候都会自动添加到这个LinkedList实例中。询问这个类的开发者,对方表示原本计划在ServiceUlr实例被回收时从该List中移除。移除的逻辑是在ServiceUrl的finilize方法中实现的,因为对象被回收前会调用finilize方法。

    到这里问题就找到了。属性LinkedList serviceUrls作为存储在方法区的类变量,是作为可达性分析的GC Root的,被serviceUrls引用的对象是不会被回收的,所以内存中所有的ServiceUrl实例都不会被回收,serviceUrls占据内存会持续增加,且不会被释放,导致了内存泄漏。

解决方案及建议:

    1)使用Map存储ServiceUrl的实例,合理设置key值的组成;

    2)我们一般声明对象的方式,声明的是强引用类型的对象,只有当迫不得已的时候(JVM无法给新的实例分配内存空间时)才会被回收,不应该将正常的功能实现寄希望于垃圾回收;

    3)finilize方法中不应该有重要的逻辑。

你可能感兴趣的:(Java虚拟机)