OOM分析之问题定位(一)

一.问题描述

最近对系统进行压测时发现Server日志报如下错误:java.lang.OutOfMemoryError: GC overhead limit exceeded。

查看Oracle OOM文档,在文档中找到以下内容:

Exception in thread thread_name: java.lang.OutOfMemoryError:GC Overhead limit exceeded

Cause: The detail message "GC overhead limitexceeded" indicates that the garbage collector is running all the time andJava program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown.This exception is typically thrown because the amount of live data barely fitsinto the Java heap having little free space for new allocations.

Action:Increase the heap size.The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with thecommand line flag -XX:-UseGCOverheadLimit.

原文链接如下:

https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks002.html

上面一段文字大概意思是JVM用98%的时间回收了不到2%的堆内存,因此预测到内存即将耗尽,抛出OOM异常来提示内存不足,相当于给用户一个警告信息。Oracle给的解决方法是提高堆内存,也可以用-XX:-UseGCOverheadLimit参数来关闭这个异常警告。

这种方式其实只是治标不治本,让用户提高堆内存容量,虽然解决了燃眉之急,但实际上并没有解决内存消耗过大的根本问题。如果堆内存一直增长,最终还会提示“GC overhead limit exceeded”。即使用XX:-UseGCOverheadLimit参数关闭OOM预测提示,当内存占用增长到极限,最后也会直接报 java.lang.OutOfMemoryError: Java heap space,造成进程停止的严重影响。所以Oracle给的解决方案并不靠谱,只能作为参考。

通过以上描述,可以推测进程中要么有任务一直在申请内存,要么发生了内存泄露。具体哪种原因还得对内存使用情况深入分析后判断,以下是分析的整个过程。

二.MAT分析

1.获取dump文件

登上服务器,用 Java自带的jmap生成dump 文件,命令如下:

jmap -dump:live,format=b,file= heap.hprof <pid>

2. 饼图分析

将dump文件下载到自己电脑,用 MAT打开。

从overview视图看到java.util.concurrent.ThreadPoolExecutor @ 0x744ce0aa 共占用了1.6G内存,比例占到了75%,可以初步判断有一个ThreadPoolExecutor的实例占据了大量内存,导致OOM。

3.Leak Suspects报告

点击overview视图下的Leak Suspects 按钮,查看分析结果。

从报告中看到QRCodeBatchTask类中引用的ThreadPoolExecutor的对象共占用了71.43%的内存,java.util.concurrent.LinkedBlockingQueue$Node 对象占用的最多。

4. Histogram图

点击菜单栏的Hitstorgam按钮,生成instances报告,并用Retained Heap进行排序。

图中Shallow Heap指对象自身占用内存大小,Retained Heap指对象自身加其引用对象总共占用内存大小。

从图中看出占用内存最多的是BufferedImages对象,QRCodeTask对象排到了第四,这个对象功能后面会提到。

选中BufferedImage右键查show object by classby incoming class,看到确实是QRCodeTask引用了BufferedImage。

选中BufferedImage右键查看Merge Shortest Paths to GC Roots ->exclude all phantim/weak/softetc. references,可以得知QRCodeTask中的的backGroundImage,logoImage变量占用内存最大。

5. DominatorTree

选中BufferedImage,点击Dominator Tree菜单。从图中可以看到确实是java.util.concurrent.LinkedBlockingQueue$Node引用了BufferImage,与Leak Suspects报告一致。

至此,可以基本分析出是QRCodeTask类中的两个BufferImage 对象backGroundImage和logoImage占用了大量内存,大概17M。

MAT分析工作基本到此结束,下一步找到相应代码查看业务逻辑。

三.代码分析

1.当有请求过来时会执行以下函数,把QRCodeBatchTask添加到线程池中。

public void applyQRcode() {    exec.execute(new QRCodeBatchTask());  }

2.QRCodeBatchTask会不断往线程池提交QRCodeTask任务,并把生成的图片赋值给QRCodeTask。

public void run() {backImage=ImageIO.read(backGroundImageFile);logoImage=ImageIO.read(logoFile);for (List list : lists) {exec.execute(new QRCodeTask(backImage,logoImage));)}}

3.QRCodeTask保存backGroundImage,logoImage对象,并在任务执行中用到。

public class QRCodeTask implements Runnable {   

           private BufferedImage backGroundImage, logoImage;   

           public QRCodeTask(String backGroundImage, logoImage ) {       

           this.backGroundImage = backImage;       

            this.logoImage = logoImage;   

}

}

以上业务代码处理逻辑是每次请求过来时都会新建一个QRCodeBatchTask任务并提交到线程池中。QRCodeBatchTask会从磁盘读取两张图片,赋值给新建的QRCodeTask任务,所有新建的任务都会提交到线程池中去执行。

ThreadPoolExecutor原理是把提交的所有任务添加到LinkedBlockingQueue队列中,这也就是图中java.util.concurrent.LinkedBlockingQueue$Node对象引用QRCodeTask对象的原因。实际压测时候随着请求的持续增加,新建的QRCodeBatchTask越来越多,生成的图片也越来越多,最终导致占用的内存持续增长直至OOM发生。

四.问题解决

原因已经知道,解决问题就相对容易了。因为每个任务用到的backGroundImage,logoImage其实是一样的,因此可以用单例模式获取这两个图片,以此来保证进程内只保留一个图片对象,这样就减少了内存的占用。

通过重构代码,再次运行程序并用VisualVM进行监控可以观察到内存占用趋于平稳,没有再发生OOM,问题得到解决。

如果想获得更多,欢迎关注公众号:七分熟pizza

公众号里我会分享更多技术以及职场方面的经验,大家有什么问题也可以直接在公众号向我提问交流。

![qrcode.jpg](https://upload-images.jianshu.io/upload_images/292732-63fecfd39bff3189.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342