作为Java开发者,相信大家或多或少都遇到OutOfMemeoryError这个错误。怎么处理这个问题,各位经验可能都比较丰富,网上也很多定位和解决这个问题的方式。不过今天我要说的是实际工作中遇到的一次不一样的问题定位及处理方式。
现象
有个大屏页面,运维说用户反馈过几天就需要重新登录。
问题定位过程
1. 遇到这种问题,第一反应,肯定怀疑是不是session过期(根据不同系统可能是cookie,token之类的)。然后和前端对了一下,确定有定时请求接口,又看了下后端超时时间,如果有定时刷新,肯定不会因为过期导致重新登录。这怎么办?说实话我也不知道该怎么办了,只能让用户继续用着,看有没有可能再出现这个问题,再来定位处理。
2. 没过几天,运维人员又反馈,系统进不去了。赶忙上去看了,结果发现程序进程已经挂掉了。赶忙把程序运行起来。然后准备去看看是什么原因导致程序挂了,首先看了下业务的日志,看了一圈下来,没发现有什么特别的,连个异常或者ERROR都没有。 回到jar包目录,发现目录下面有个hs_err_pid开头的log文件,以前看到过类似的文件,知道确实JVM崩溃了。立马打开文件看看 好家伙,没看几行,就看见了Out Of Memory Error。那么问题已经确定了,就是内存不足引起的JVM崩溃。处理这种问题,就是常用的那些手段。结果弄了一圈下来,没发现哪里有内存泄漏。那只有先扩大一下内存试试,可能内存确实就不够用。(其实以前遇到产生这个文件的时候,也都是内存不足,有的时候是确实有内存泄漏,有的时候就是内存本身确实设置得小了一点,但是这次有点不一样得是业务日志中没有一丝一毫的相关信息,以前遇到的情况,不管是哪块区域内存不足,在业务日志中都有OutOfMemory)。
3. 没过几天, 早上刚到公司,又反馈程序又访问不了了,上去看了一下,又是同样的现象,进程死掉了,同样产生了hs_err_pid文件。看了下崩溃文件产生的时间,居然是在半夜,这下我就更纳闷了,我们客户是政府行业,按道理说,半夜是不会有人用的。要崩溃在白天用的人多的时候崩溃我还理解。这次认真看了下hs_err_pid文件,看到下面这张图片的时候,我震惊了,老年代内存才用了18%,感觉上不应该是内存不足。 于是又回到文件开头,看看OutOfMemory附近有没有什么可以用的信息, 然后看到了下面这句话,感觉有点关系,但是要怎么处理呢,没遇到过,确实也不知道咋处理,于是网上搜索。 看到了篇感觉比较靠谱的文章。 # 记一次kafka集群频繁crash的排查过程 ,从源代码角度分析了问题所在,于是照着他的解决访问设置了一下操作系统参数。 此时我反应过来,以前大屏过一段时间就要登录就是因为这个程序了,因为程序半夜崩溃了,等上班的时候客户端来的请求会话已经超时了,自然就会让其重新登录。 为了尽量避免大屏用户重新登录,和一上班系统就无法使用这种情况,于是写了个脚本,定时检查程序进程,如果不在了,就自动拉起来。 4. 本来以为问题彻底解决了,万万没想到,有天运维说又访问不了了,因为有别的事情在忙,我就告诉他让他先去启动一下,先用着。 结果过了一会说,还没有重启又可以用了, 回想其做了定时检测的脚本,只是时间设置了10分钟一次。应该是系统检测到了,自动拉起来了。 等我空了的时候,果然又是JVM崩溃过,而且业务日志里面也没有错误信息。又去看看内存,结果发现比以前翻了更久才到了第三次看内存的那个位置,而结果也是一样,内存看着是没啥毛病。 这位置前面全是线程信息,仔细一看,线程池都几千个了,而且每个线程池里面有几十个线程。
这回知道了,肯定哪里用了线程池没有释放,以前那几种处理方式确实没办法从根本上解决问题,顶多只是延缓事情的发生。接下来就是看代码,果然发现了有个地方使用了固定数量线程池,而且是根据指标数量来设置线程池数量,查了下数据库,指标确实是有好几十个。而用完了之后并没有关闭线程池。
总结
1、临时创建的线程池,不用了的时候一定记得要关闭。当然肯定有的朋友会说,用初始化的时候创建的线程池,可以肯定倒是可以,不过看具体情况吧, 用少量的线程池必然会导致后面的请求等待。我们这个场景,其实用CachedThreadPool比较合适。
2、实践才是检验真理的唯一标准,光看现象,我相信没知道几个人会想到最终是因为这个问题导致的。所以面试的时候,我更喜欢问一下大家实际中有没有遇到什么问题,最反感的就是网上的那一堆八股文。
3、其实我知道大家肯定还想吐槽一些处理方式,比如出了问题就重启什么的,或者定时检测的脚本时间久了,连个容灾都没有,一台挂了就挂了。这个怎么说呢,还是看具体情况,像我们的用户,要求没那么高,当然资源也没那么充足,一台服务器上都跑了很多个应用。