一次tomcat内存溢出问题的排查以及引出的dump文件分析

匿名 (未验证) 提交于 2019-12-02 23:26:52

问题的现象:

因为个人对tomcat不熟悉,所以网上找了下tomcat假死的原因,大致有如下几种:

与tomcat连接未关闭/长连接数超过最大连接数

  1. Load过高,超出服务器极限
  2. 应用程序出现死锁
  3. JVM GC时间过长,导致应用暂停/JVM内存溢出

按顺序一个一个来排查:

  1. top一下, load average不高,说明不是服务器负载太高了
  2. 看下是否是死锁的问题

    然后使用vim找了下“DEADLOCK”,没有找到,应该是没有出现死锁(最好dump多次,才能更好的定位问题)

  3. 使用visualvm查看下tomcat进程的堆栈情况。(怎么使用visualvm看我之前写的那篇文章-“使用visualvm远程监控Java程序”),发现堆内存占用很大。一段时间后,日志里面也报了错误:

下面的图是我在出现内存溢出后截的:

那么接下来就是分析是哪里导致内存溢出了:

先把heap信息导出来:

使用mat工具分析:(mat网上随便下吧,我想传上去骗点分,老是显示资源已经有了…)

./ParseHeapDump.sh ../tomcat.bin org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components

会出来这么三个文件,里面都是html网页,记录了堆信息。

主要是看TopConsumer,这里记录了内存消耗大户:

这里判断那里内存溢出,需要结合代码以及堆栈文件了。结合工程的逻辑,这里一看就知道是多线程的问题,线程太多并且每个线程要拉取的HBase数据太多造成JVM内存溢出。

dump文件分析介绍

虽然dump文件中没有死锁,但是看懂dump文件对于了解程序运行状态还是有帮助的,所以这里介绍下dump文件中的内容。如果有说的不对的地方,麻烦大家及时指正下呀!

如果问题不好定位的话,需要dump多次,每次间隔个几秒钟,然后结合着一起看。

Dump文件结构大致如下:

JAVA EE部分。(具体信息详见“参考3”,忽略上面的weblogic.Server.main部分,那是参考文章作者自己的内容)。

来看下JAVA EE这部分的内容,从堆栈文件中可以看到线程会显示两种状态,一种是操作系统线程状态,一种是JVM线程状态(在Thread.State枚举类中有说明):

JVM定义的六种状态中来(在java.lang.Thread.State类中)。接下来分别细说下JVM线程状态。

JVM线程运行状态:

NEW:

线程刚创建且未启动

RUNNABLE:

线程处于运行状态

BLOCKED:

线程等待获取监视器锁(monitor lock)以期进入同步代码块/方法(即等待锁,并且是synchronized)中。(下面会说到Monitor中的Entry Set和Wait Set,BLOCKED还有下面即将说到的WAITING和TIMED_WAITING都和这两个队列有关)

WAITING:

线程无期限的等待另外一个线程来执行一个特定的操作,等待另外一个线程调用notify()/notifyAll()。特定操作可以是:Object.wait()、Thread.join()、LockSupport.park()

TIMED_WAITING:

线程在一段时间范围内等待另外一个线程来执行特定操作,时间到了之后又回到运行状态。特定操作可以是:Object.wait(time)、Thread.join(time)、LockSupport.park(time)、Thread.sleep(time)

Ps1: sleep可以在任何地方使用, wait、notify、notifyAll只能在同步控制方法或同步控制块中使用;sleep不会释放锁,但是wait会;结合以上两点,WAITING状态结束之后会转为BLOCKED状态,因为它是要重新取获取锁的。

Ps2:ReentrantLcok中未获取到锁时调用的是park/parkNanos/(进入定义的队列),所以用ReentrantLock是进入到WAITING/TIMED_WAITING状态。

TERMINATED:

线程运行结束

Entry Set & Wait Set介绍:

每个Java对象有且仅有一个Moniter,Entry Set和Wait Set属于这个Moniter的两个队列,又可以称为锁池和等待队列,他们是Java中用以实现线程之间的互斥与协作的主要手段,可以看成是对象或者class的锁。等待队列中的线程不能去争取锁,只有晋升到锁池队列中的线程才能去争取锁,这个就是两者的区别。如果对象调用的是notify,那么只有一个线程能进入到锁池队列,如果调用的是notifyAll,所有线程才能全部进到锁池队列中一起争夺锁。

实际一些日志中的信息分析:

BLOCKED状态:

正在等待获取锁0x0000***188

CPU占用高,load高,响应很慢:

CPU占用不高,响应慢:

BLOCKED状态的越来越多,很有可能被一个大锁锁住了

WAIT状态越来越多,那么可能是线程停在了I/O,数据库连接或者网络连接等地方

或者直接就是两者很多

死锁:

参考:

https://www.javatang.com/archives/2017/10/25/36441958.html(Dump文件如何分析)

https://www.jianshu.com/p/8fe0b117cacd(Java线程状态和系统线程状态区别)

https://www.oschina.net/translate/jvm-how-to-analyze-thread-dump?print(dump文件结构图)

https://www.javatang.com/archives/2017/10/26/08572060.html(dump文件一些典型案例)

文章来源: https://blog.csdn.net/zc19921215/article/details/89682422
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!