一起SQL引发OOM的解决思路与过程(转载)

在TOMCAT WEB程序的运行过程中,突然触发了内存溢出错误,检查Tomcat的localhost日志,找到如下信息:

java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Arrays.java:2245)
        at java.util.Arrays.copyOf(Arrays.java:2219)
        at java.util.ArrayList.grow(ArrayList.java:242)
        at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
        at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
        at java.util.ArrayList.add(ArrayList.java:440)
        at com.mysql.jdbc.MysqlIO.readSingleRowSet(MysqlIO.java:3442)
        ……
        at com.yiifaa.indicator.impl.HoleIndicatorServiceImpl.expireHoles(HoleIndicatorServiceImpl.java:191)
        at com.yiifaa.indicator.controller.HoleIndicatorController.expireHoles(HoleIndicatorController.java:69)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

按照提示,找到代码进行检阅:

Map<String, Object> params = Maps.newHashMap();
String sql = sqlCache.get("expireHoles");
//  这里就是191行
List<Map<String, Object>> results = this.jdbcTemplate.queryForList(sql, params);
  • 1
  • 2
  • 3
  • 4

看了好几遍,没有发现明显的异常,既没有全局变量,也没有属性变量,没理由出现内存溢出,更重要的是,按照以往的经验,SQL应该没有这么大的爆炸力,而且难以确定的是,到底是该方法引发了OOM异常,还是碰巧就遇到了内存的瓶颈,从而导致内存异常,所以需要进一步判断。

刷新页面,调用应用程序接口,同时使用jstat监测JVM内存使用,如下:

# 首先获取程序ID
jps -l
# 然后根据程序ID获取其JVM内存使用信息
# 10代表时间间隔,毫秒
jstat -gcutil 47279 10
  • 1
  • 2
  • 3
  • 4
  • 5

执行jstat命令后,发现如表1的现象:

S0S1E(年轻代占比)O(老年代占比)P(永久代占比)
不变 持续增长 持续增长 持续增长 不变

增长的顺序依次是E(年轻代占比)增长到100%,然后S1增长到100%,然后O(老年代占比)增长到100%,最后触发full gc(全局垃圾回收),并且在内存变化的这段时间,并没有进行任何操作,连接数量也在比较小的范围。

#   统计HTTP连接的活动线程
jstack 47279|grep http-bio-8080-exec|grep runnable|wc -l
#   统计HTTP连接数量
 netstat -nat|grep -i '8080'|wc -l
  • 1
  • 2
  • 3
  • 4

上述两个命令相互论证,证明HTTP连接是正常的,继续用jstack进行诊断,发现很多HTTP线程处于“WAITED”状态,这说明都是处于连接池中,等待被激活,依旧正常。

继续推测,内存的增长必然伴随对象数量的增加,于是采用jmap进行诊断,如下:

jmap -histo 47279|less 
  • 1

持续多次运行此命令,发现有个对象数量以及增长速度都不正常,如下:

1716115  41187603  com.mysql.jdbc.ByteArrayRow
  • 1

难道真是数据库连接的问题?突然想到,消耗内存较多的代码一定是运行中的线程,于是用jstack获取到的信息依次诊断线程,发现真有个数据库线程不正常,如下:

http-bio-8080-exec-89" daemon prio=10 tid=0x00007f039c09f800 nid=0xbcd1 runnable [0x00007f05350cc000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.read(SocketInputStream.java:152)
        at java.net.SocketInputStream.read(SocketInputStream.java:122)
        at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:114)
        at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:161)
        at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:189)
       ……
        at com.yiifaa.sec.indicator.impl.HoleIndicatorServiceImpl.expireHoles(HoleIndicatorServiceImpl.java:191)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

发现程序依旧卡在HoleIndicatorServiceImpl类的191行,最后诊断SQL语句,发现少了一个括号,导致or语句连接数量增长极快,从而是数据量达到了10的22次方,最后引发了如此大的执行灾难。

继续细化分析下去,因为返回结果是个List容器,所以随着游标的滑动,导致数据集不断增长,从而使得“com.mysql.jdbc.ByteArrayRow”对象数量暴增,最后导致内存溢出,结合内存的增长趋势,可以明显看出,当方法内部的对象增大到一定程度时,将会直接进入幸存代(S1),甚至是老年代。

总结

从内存的增加现象看: 
1. 只要对象足够大,完全有可能直接进入老年代,即使实在局部方法内; 
2. 局部方法也可能会导致内存溢出;

从诊断过程看,相关工具的作用如下: 
1. 用jps获取jvm进程ID; 
2. 用jstat确认GC内存消耗速度; 
3. 用jmap确认对象的增长速度与耗费空间; 
4. 用jstack确认错误代码的位置。

从SQL的教训看: 
1. 在多表连接时,请仔细检查连接语句; 
2. 万无一失的做法,请限定抓取数据的数量,例如100条,如“limit 0, 100”。

 

原文地址i:https://blog.csdn.net/yiifaa/article/details/78648358

posted @ 2018-07-13 01:17  draem0507  阅读(432)  评论(0编辑  收藏  举报
View Code