Bootstrap

JVM系列之OOM实战

      工作以来,陆陆续续遇见Java应用OOM情况非常多了。每次未定位到原因时总是让人战战兢兢,特别是发生在交易频繁的系统身上,压力更大。因此,编写代码的时候,不时问问自己,我们这样写,会不会引发OOM。分享一篇关于OOM的经历,希望能给大家带来一点解决问题的灵感吧。

      导致OOM的原因很多,有堆内存溢出,有元空间溢出,有栈溢出。但是堆内存溢出最为常见,其中堆内存溢出中以大对象或者常驻内存无法被回收(内存泄漏)导致最终内存溢出最为常见。当然,所谓的大对象也是相对虚拟机分配的内存来说的。但在编码的过程中,我们尽量避免大对象的产生。

       大概在两年前的一个周五晚上,正好是交易相对繁忙的时候,笔者亲身经历一个生产OOM故障,至今印象十分深刻。因为交易超时触发告警,去机房一看日志和机器情况,发现已经一台主机的微服务应用A已经OOM了,然后约1小时之后,另一个主机的微服务应用A也OOM,通过翻看日志,发现溢出和业务逻辑代码没有特别明显的关系。第二天上午,备机房的两个应用A也相继触发了OOM,下图是测试环境模拟出来的OOM日志,可以明显看到java.lang.OutOfMemoryError:Java heap space的溢出信息。

      这是使用jvisualvm观察应用在测试环境慢慢溢出的内存使用情况,可以看出内存可以回收的越来越少,逐渐上涨,这是非常不健康的回收情况,如下图:

      通过jstat工具来查看应用的回收情况,也能看出溢出的情况,如下图:

      通过top命令可以看到第进程CPU飙升,通常内存溢出时,都会表现CPU飙升,其实这时CPU都在忙着回收基本无法回收的垃圾,溢出啦。如下图:

通过用mat分析Dump错误信息如下:

       根据日志报有OutOfMemory类型错误,分析有存在内存泄露,dump文件的错误类型有dubbo应用包占比过多,还有finalize优先级低于GC的线程处理占用过多内存。仔细检查项目代码,未发现应用程序层面的问题。随后安排进行了较长时间的稳定性测试。生产极可能为多天积累导致的内存泄露。

     测试时为了较快复现问题,在服务端设置线程sleep三秒钟,并加大并发量,缩小虚拟机内存。测试工具为loadrunner,监控工具为jvisualvm、jconsole。

测试结果如下:

两个多小时的持续压测,系统老年代的堆栈一直居高,不能正常回收。

jconsole观测如下图:

     运行12个多小时后,内存耗尽,堆内存不能正常回收,检查dump等文件,测试环境的日志报错和生产也完全一致,算是把问题重现了。使用jvisualvm观察如下图:

     老年代的内存不能释放被回收,后仅对应用A进行性能测试,不通过dubbo连接微服务,一切正常,老年代堆栈可以正常回收。判断极可能为dubbo相关包的问题。同时,通过mat对dump日志分析,也指向dubbo相关jar包,后搜索相关类,和排查dubbo相关包源码,发现spring引入的dubbo关联包为快照版,包名为spring-xxx-dubbo-xxx-SNAPSHOT.jar,该包为非正式版,通过排除相关类,发现有做了一些统计操作和通讯加上了同步锁(synchronized),资源释放慢,很容易导致老年代内存回收失败。

替换为产品之前确认的dubbo-spring-xxxx.jar正式包,同样是加大并发,缩小内存来压测,测试情况如下:

运行3个小时后,堆栈内存正常回收一次

运行7个小时后,堆栈内存正常回收

       运行20个小时后,堆栈运行依然正常稳定,看到规律的回收情况,心里的石头算是落地了。

       后记,在实际的生产中,内存溢出问题的原因很多,但是大都因为使用对象不当。例如查库一次性获取数据量大导致大对象产生,这时溢出异常日志往往上面是有明显的业务逻辑代码的操作。这类OOM比较好处理,通过优化业务逻辑代码即可。像本次非逻辑代码相关的,还需要借助mat来分析dump,并逐步缩小范围排除来找出问题根因。


 

                         

;