OOM排查

引言 每个Java程序猿在成为大湿的路上都会遇到OOM的问题。知晓OOM如何自救,不但可以在面试时如添神翼,还可以在服务报警时力挽狂澜。 OOM是什么? OutOfMemoryError,内存泄露或溢出错误。Java将内存管理全权交给了JVM,每个对象生命周期中内存分配和收集都由JVM负责,程序猿无需编写每个对象的free/delete。此时如果程序编写不当或机器配置不足,JVM无力为新对象分配空间,就会触发OOM。 本文假定读者已了解JVM的基本知识。 一, 排查 上面已经提到,OOM触发时机是JVM在GC回收后也无法为对象分配空间,先说下回收标准和GC策略吧。对我们的程序而言,一个对象如果没有来自GC Roots的强引用就被标识为可回收。在G1收集器出现以前,JVM都是根据generation分代进行GC。一般绝大多数对象的生命周期只发生在新生代,经历了数次新生代的minor gc依然存活的对象被移到老生代。等老生代被填满就触发major gc。Major gc针对整个堆空间,对象数据量更大,并且有压缩整理。所以major gc远远比minor gc停顿时间要长。 如果启用的JVM参数和GC策略不合理,或应用编写不当,导致强引用指向大量对象,这些对象一直得不到回收,新生代和老生代空间被撑满,JVM无论怎样GC都不能为新对象腾出足够的空间,就触发了OOM。OOM出现后,GC线程频繁调用占用了CPU Time,导致用户线程长时间得不到响应。 二, 离线分析 OOM属于比较严重的线上事故,需要尽快拿到内存泄露时的heap镜像(dump日志),分析触发内存泄露的原因。 2.1为什么离线分析 与应用同步运行的java内存和性能分析工具,一般采样或剖析,记录线上的方法创建热点,对象创建热点和线程,对系统性能影响较大。如淘宝推出的TProfiler,优化后在低峰期对应用响应时间影响20% 高峰期对吞吐量大约有30%的降低。 2.2如何获取dump日志 JDK版本1.4.2_14后,sun添加了参数XX:+HeapDumpOnOutOfMemoryError,在OOM触发时自动dump日志。同时JDK tools.jar也提供了jmap工具,在系统响应变慢时手动dump虚拟机的heap镜像,从gc异常的角度进行分析,jmap -dump:format=b,file=/var/logs/dump_project_host_20140309.log。同时jmap也可以直接对镜像进行分析, jmap –histo pid。 2.3选择分析工具 Mat(Memory Analyze Tool), 是一个基于JVM的内存镜像分析工具,使用图形化 界面直观展示分析结果。可以在Eclipse集成安装,也可单独安装。由于Mat本身也是JVM管理的一个进程,启动时JVM参数配置了一定的内存空间。如果要分析的日志较大(现在dump日志动辄4,5G),默认的JVM配置不够用,需要重设 MemoryAnalyzer.ini中的-Xmx参数,否则会有load异常。 其他离线分析工具包括:Jhat, Jprofile ,Jprobe,但是没有图形化界面,或者对大文件的分析力不从心,因此更多的是采用Mat。地址: http://download.eclipse.org/mat/1.3/update-site/ 2.4分析步骤 目前公司的Java服务一般配置在Linux服务器,如果服务无响应,但机器和网络正常,则需要排查CPU和存储空间。但如果服务器单点,第一步骤是重启服务。 1 登录到事故机器。top查看机器负载。cpu负载>1就已经超出正常范围,理想的load在0.78(经验值)。 2 如果cpu负载过高,需要查看具体吃cpu资源的线程。指令: top -H -p pid 3 查看系统进程的线程栈信息。指令 jstack -l pid. 可以看到所有线程的调用堆栈,重点查看处于runnable,blocked状态的线程。也可以直接使用步骤2得出的高负载线程号直接查看,但首先要换算为16进制。指令: python -c 'print hex(thread_id)' 4 查看统进程的gc信息。指令: jstat -gcutil pid 1000 10 每1000ms打印1次,共打印10次。 5 dump系统的内存信息,最好标注时间和服务器host,方便后续追踪。指令:jmap -dump:format=b,file=/var/logs/dump_project_host_20140309.log。 6 将dump日志取到本地。dump日志较大,最好使用zip压缩后再download,另外dump时机必须及时,错误的时机将会引向完全错误的分析。 7 在本地使用mat进行分析dump下来的日志。 三 线上堆溢出实例 1> 3月6号收到线上服务监控Check Alive进程无响应,登录到线上机器看到进程还在。使用top+jstack分析占用CPU的主要线程 都是GC: 再使用jstat按照2.4中步骤分析。正常情况下合同进程每几天会有一次Full GC。和上边jstack结果结合,现在就可以判断应该是内存分配回收出了问题。 2>使用Memory Analyzer打开dump日志,饼图中深色区域为内存泄露疑点。 Problem Suspect1线程持有的内存接近进程总内存的67%,导致近1G的空间无法回收,对象类型为LinkedList。很明显该内存非ehCache等内存缓存(否则类型为net.sf.ehcache.Cache),初步怀疑在这发生了泄露,接下来再看线程调用栈‘See stacktrace’ 回到工程检查代码,确实有大量的LinkedList在方法内部创建,但没发现有明显的漏洞导致该局部变量被静态引用得不到释放。没关系,继续看。在Problem Suspect1的Detail中看到有具体的对象描述,看看list的具体内容吧,右键--‘Go Into’。 可以看到LinkedList的元素类型是HashMap,每个hashmap持有size为3的table。继续‘go into’看Entry具体的键值对,3个key分别为oldvalue, newvalue和key,判断得出hashmap存储的是合同的一条修改记录。 四 解决 目前工程里有部分业务逻辑依赖于当前http请求Thread,这种超大规模的修改记录应该是对数据批量处理导致。另个一个细节:Entry中name大部分为空,推断出系统某些数据库字段在配置时定义不完全,缺少value到name的对应,导致取值失败。至此该泄露分析完毕。解决方法:约定需求,从业务上避免这种大批量修改记录的读取,对现有其他字段的定义进行补全。

猜你喜欢

转载自arrowolfox.iteye.com/blog/2034614