【内功修炼系列2】排“O”解难

2,250 阅读9分钟
title图

导语

大家好,我是热心的朝阳群众。

最近在一些开源社区群里,看到有些同学线上环境出现OOM,急的在群里求救。求助同学首先截了一个日志系统里的输出,异常堆栈大写的OOM异常信息。这位同学说道:“今天重启了几次服务,可能是内存泄漏,但是没找到是哪里泄漏了,这个要从哪里开始排查呢?”。大家可能在开发过程中都遇到过代码写的不好,导致内存泄露,最后搞的整个服务OOM,当然这个OOM是统称,JVM的4块空间都会发生OOM,这个我在前面的文章:《【内功修炼系列2】给JVM把个脉》,有提到过,不熟悉的同学可以仔细阅读。

我们所说的OOM是JAVA开发人员比较常见的问题,也是面试过程中必考点。我个人而言,之前在很多年前就开始与其对抗,也经历过由于多线程处理、集合类引用对象不当、递归循环等场景,导致线上OOM服务挂掉,惹火上身。记得当初还是单体应用的时候,有个同事通过log4j写日志时增加业务处理逻辑,但是在appender环节没处理好,导致用户量上升的时候线程中没有回收对象导致内存泄露。刚开始就不断的给Tomcat调整参数,增加JVM的启动内存。以至于一段时间内,一旦遇到OOM就要重启加内存,但是总有于事无补的时候,最后还是从代码层面做修改,找到内存泄露的根本原因,彻底根治。

今天咱们就一起看看引起OOM的原因,以及一些排查的方法,帮助大家排“O”解难。

OOM相关概念

我们先和大家一起复习下OOM的相关概念:

OOM

Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector.

OOM全称是OutOfMemory,官网翻译过来是:JVM由于没有足够的内存来为对象分配空间,垃圾回收器也已经没有空间可回收时,就会抛出这个错误。

内存泄露

申请使用完的内存没有释放,一直不能被回收,导致JVM不能再次使用该内存,此时这段内存就泄露了。这就是“自己不用,也不给别人用!”。

内存溢出

程序申请的内存超出了JVM能提供的内存大小,这就会出现内存溢出。内存溢出大部分情况下是由于内存泄露导致,还有就是流量确实大,服务没有做弹性扩容、服务限流,导致内存溢出。是真顶不住啦!

OOM的日常

由于做医疗业务,业务高峰期间会发生在周一早上,早在服务架构升级之前的单体应用时期,由于开发人员水平参差不齐,而且所有业务都耦合在一起。尤其业务增长期,时不时会出现OOM,服务器CPU占用过高等故障。运维人员或者是研发人员,盯着服务器监控,内存很多情况就是一直向上涨,快要支撑不住了,就重启服务。真是“人工智能运维”啊!当时是重启服务的同事,把内存快照、线程快照dump下来,发给研发人员分析。那段时间运维人员也是苦不堪言啊!

苦逼运维

后来随着业务发展,人员壮大,也逐渐转微服务架构。在实现服务自动扩缩容之前,也是个别服务偶尔会出现OOM这种情况。当然我们的监控系统也设置了阈值,例如:每当服务超过80%内存时,负责运维同事、研发同事,都会收到短信、邮件、IM消息的一套告警。或者某个服务还来不及处理,已经OOM,服务进程挂在那里,自动导出快照,运维同事会把快照发给相关研发同事进行分析,发现还是会出现内存泄露导致OOM的情况。

排查OOM

接下来就结合我们目前的情况,介绍一下处理OOM的方法。

1.服务启动加参数

image-20201003190609048

这个是我们在git上的,每个服务的Deployment.yml配指文件,都有增加当OOM出现,会将快照文件导出到DUMP_PATH路径下。之后通过jenkins执行jenkinsfile,执行docker镜像下的sh脚本,进行服务启动。当然也可以通过脚本设置等方法,我相信每个公司都有所不同,但重要的要加上-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath这两个启动参数,这样在出现OOM的情况下,也可以保存当时的快照,以辅助研发人员进行分析。

2.排查日志

当发生OOM的时候,监控系统会发出警告,我们会收到短息、IM消息等一堆,这里就不截图了。反正就是自己定义的一些告警信息。之后运维修复服务的时候,我们会根据消息去Kibana查询相关日志。

kibana查询oom

这里会切到相关服务的filter下,查询服务日志,当然我这里的图只是模仿一下,查询OOM是没有数据的。实际查询日志的时候,如果是服务发生了OOM,是会有相关堆栈信息的。

3.运维定位

运维收到预警信息的时候,就会到服务器进行排查。首先定位服务的问题,也就是平时我们排查服务器的一些命令,例如:top、jps、jinfo这些基本操作。定位到相关服务之后,如果需要线上分析,基本会找相关开发人员排查,包括堆栈、gc、线程情况等相关信息。开发人员就会用jmap、jstat、jstack逐个排查。如果确实出现了OOM,那这时候运维会把快照拿下来,发给开发人员分析。

4.OOM分析

模拟OOM

这块因为近期系统没有出现OOM,并且手头上没有实际案例的内容(因为OOM是很久之前发生的)。所以就准备写个demo,一起看下排查的步骤和方法。我前面也说过,OOM有时候并不是代码引起的,也可能是参数设置不当、流量大等原因造成的。不过今天主要是说的代码内存泄露问题,导致内存溢出。而且JVM除了程序计数器以外,堆、虚拟机栈、方法区、本地方法栈都会出现OOM。由于我们日常内存分析使用的是MAT(Eclipse Memory Analyzer,这里不用装eclipse,可独立安装),所以今天就用这个工具演示一下。

这里以堆内存溢出为例,直接上代码:

代码

这里直接用大对象的方式,粗暴的死循环,直接向堆内存塞10M的对象。启动内存设置:-Xms10M -Xmx50M ,并设置OOM时生成快照,并保存至指定路径:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=具体路径

具体设置如下图:

启动配置

当然如果是本地调试,也可以用JProfile进行监控,可以实时拿到服务的线程、内存等情况,不过这种要开放服务端口,并实时去拿,会给服务带来一些压力。记得当年单体应用时,还用过jprofile直接连服务排查问题。

Jprofile

运行结果:程序运行了8次,就直接堆空间溢出了。由于设置了相关参数,输出了:java_pid53518.hprof文件。

运行结果

排查思路

这里先说一下使用MAT排查具体思路:

  1. 通过MAT视图查看消耗内存最大的类型,大致来分析内存泄露的原因;
  2. 查看大类型对象明细列表、引用链,来定位内存泄露的具体点;
  3. 查看对象属性的值和依赖关系,梳理出程序逻辑和参数;
  4. 通过线程查看,定位 OOM 问题是否和过多线程有关;
  5. 定位到相关代码,改掉。当然可能很悲剧,你没排查到。。。。

MAT排查

  1. 启动MAT:

mac系统点击安装启动会报错,所以用命令直接启动:/你的路径/MemoryAnalyzer -data ./dump

MAT启动报错
  1. 加载hprof文件
MAT主页面1

打开MAT主界面,是一个overview界面,可以看出OOM的时候,整个堆的大小为32.6M,有对象占用了32M。这时候我们要看下对象的情况。

  1. 查看对象列表
对象histogram

刚才我们看到程序的运行结果是8次。这里正好有8个对象,占用了32M,这正好符合我们程序的预期结果。接下来要看看这些对象都是什么?有多少实例?对象都是在哪个线程下运行的?

点击大对象,右键,进入with incoming references。

查看对象操作
  1. 查看对象结果图
list-object

这时候看到大对象是ArrayList里的对象,Retained Heap(深堆)代表对象本身和对象关联的对象占用的内存,Shallow Heap(浅堆)代表对象本身占用的内存。这个 ArrayList 对象本身只有 24字节,但是其所有关联的对象占用了 33MB 内存。这些就可以说明,肯定有哪里在不断向这个 ArrayList 中添加对象,导致了 OOM。而且显示出是程序的主线程main里面的代码。这也与我们的代码本身预期一致。

  1. 查看线程情况
线程图

线程情况能看出,当时是主线程里面的ArrayList关联了大对象,导致了OOM的产生。其他线程也没异常,这也确实符合我们的代码逻辑。

  1. 最后一步就是改掉代码里塞入的大对象,不要死循环。。。。。问题解决。

总结

这里只是跟大家简单介绍了一下OOM的排查过程,当然在有些情况下并不是这么简单,线上监控是一个比较复杂费事的事情。个人认为最重要的是要做好代码审查、系统监控、关键步骤留痕日志。服务的监控与治理是当下微服务、云原生系统的必备能力,也体现了一个团队的技术实力。我们也在不断完善自己的监控、快速排障的能力,希望通过这个简单的思路分享,能让大家有所收获。我也会继续保持分享的态度,跟大家一起进步。

「往期文章:」

《【内功修炼系列1】线性数据结构(上篇)》

《【内功修炼系列1】线性数据结构(下篇1)》

《【内功修炼系列1】线性数据结构(下篇2)》

《【内功修炼系列2】给JVM把个脉》

《【内功修炼系列2】难啃的JIT》

特别声明

本文为原创文章,如需转载可与我联系,标明出处。谢谢!

底部通用图