记一次用jprofilor定位生产环境OOM的经历

642 阅读5分钟

本文首发于:行者AI

随着平台业务的不断发展,平台曲库数据的不断增加,系统偶尔出现内存溢出的情况。内存溢出相比其它异常而言,通常比较隐晦,一般是伴随着时间慢慢积累而产生的,因此不能仅根据异常产生处来简单定位问题,而要找到问题的根源。所以,有必要知道如何排查系统的内存溢出。本文以一次生产环境下的内存溢出为例,简单讲解如何使用jprofiler定位问题。

1. 问题

突然收到平台出现故障的消息,打开网站首页,发现所有接口均不能正常访问,页面加载失败,如下:

p0

第一时间查看异常日志,发现日志几乎全是如下内容:

p01

毫无疑问,系统发生了内存溢出。但具体由什么原因导致单单从日志是无法无法定位的。一般,对于一个稳定的系统而言,其占用的内存大小一般都会保持在一定范围内,所以内存溢出一般有以下几种原因:

  • 程序员在编写代码时,造成了内存泄露,未释放的内存逐渐积压,最终内存溢出;
  • 在代码中尝试申请较大的内存,导致大部分内存被占用,或直接导致内存溢出;
  • 系统突然的流量增大,原有的可用内存不足以支撑等。

还好,平台的线上环境均配置了jvm参数,在发生OOM时会自动dump出堆转储文件,接下来就要从dump文件入手来定位问题了。

2. 准备工作

2.1 工具

jprofilor是一款强大的JVM监控工具,提供对JVM的各种精确监控,包括内存、GC、CPU占用、线程情况等各方面的监控分析功能。对于内存溢出,通常会使用到jprofiler的堆快照分析功能,其余功能不在本文的讨论范围内。

p1

jprofiler的安装非常简单,跟着安装向导一直下一步即可,安装好运行界面如下:

p2

点击左上角的“Start Center”,在弹出窗口中可以选择不同的功能模块,其中"Quick Attach"中可以快速连接位于本机或其他主机上正在运行的java进程,进行实时监控。

p3

以本机运行的IDEA进程为例,选择"Start Center"可以看见正在运行的IDEA Java进程:

p4

选择“Start”,进入实时监控模式,可以查看该进程的各种信息:

p5

而对于内存溢出问题的排除,通常会用到jprofiler的堆内存快照分析功能,这个在后面在讨论。

2.2 jvm参数

对于线上正在运行的系统,采用"Quick Attach"方式对java进程进行监控分析显然不方便,还好jvm已经为我们增加了专门的参数,用于在java进程产生"Out of Memory"异常,即内存泄漏时自动转储堆内存快照文件。因此,我们只需要分析堆内存快照就行了。为此需要在java进程启动时加上开启此功能的启动参数,如下:

 	java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/app.hprof -jar app.jar

其中,

     -XX:+HeapDumpOnOutOfMemoryError

表示当jvm发生OOM时,自动生成dump文件;

     -XX:HeapDumpPath

用于指定生成的转储文件路径。

3. 分析jvm堆的转储文件

有了上面的配置后,就可以在系统产生OOM时进行问题排除了,此处以平台的一次OOM产生的dump文件为例,简单讲解下排查过程。

在jprofiler的"Start Center"中选择"Open Snapshots"功能中的"Open a Single Snapshot":

p6

导入成功后可以看到如下界面:

p7

其中"Classes"栏目下是按类型区分的各种instance大小,可以选择按数量或总大小排序。"char[]"类型作为"String"的内置实现通常会占用较多的内存,因此仅通过此列表并不好定位问题。此时可以选择"Biggest Objects"栏目,顾名思义,该栏目主要针对大对象。如下图:

p8

利用"Biggest Objects"功能我们能很快发现其中一个String类型的对象足足有110MB,这显然是不合理的。接下来,可以尝试根据该String对象的内容初步定为产生该对象的位置;如果仍不能准确定为问题,可以右键该对象,选择"Use Selected Objects":

p9

在弹出窗口中有"Reference"选项用于查看对象的引用,其中"Outgoing references"与"Incoming references"选项分别指查看当前已选择对象所持有的引用和持有当前对象的引用。

p10

由于String类型作为一个不可变类型,其内部实现由char[]完成,所以这里选择"Incoming references":

p11

在这个窗口中,不仅能看到对象内容和大小,还包含了对象所在线程,感觉离问题的根源又进了一步啊。接着选择"show more":

p12

至此,该导致对象产生的整个的调用栈就一览无余了。在本次OOM中,最终定位原因是业务代码中产生了一条巨大的SQL语句导致的,该部分代码在数据量小时可正常执行,由于前不久新增了大量数据而代码层面未考虑相应处理,因此出现OOM.有了这次教训,在今后编写代码时还应当多考虑避免此类情况啊!

4. 结论

总的来说,内存溢出是一个比较难于处理的问题。本文所举示例只涉及了最简单的分析场景,但通常可以发现大部分原因;对于较为复杂的OOM场景还需要结合具体实际情况。对于如何避免此类问题,还是需从代码编写入手,毕竟大分部此类问题都是程序员在编写代码时操作不当造成的。


PS:更多技术干货,快关注【公众号 | xingzhe_ai】,与行者一起讨论吧!