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

时间:2024-04-11 07:05:57

问题的现象:

    使用DHC工具发送请求,Nginx能够正常接到请求并转发,但是tomcat中的日志一直没有打印出来,说明Nginx是正常的,是tomcat没有响应请求,说明此时tomcat处于无法访问(假死)状态。

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

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

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

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

 

按顺序一个一个来排查:

  1. 执行netstat -atp |grep  **tomcat端口**  |wc –l 发现数量不多只有3,排除和tomcat端口连接未释放和连接数过多问题。一次tomcat内存溢出问题的排查以及引出的dump文件分析
  2. top一下, load average不高,说明不是服务器负载太高了一次tomcat内存溢出问题的排查以及引出的dump文件分析
  3. 看下是否是死锁的问题

    ps –ef |grep tomcat,先找到tomcat对应的进程号

    jstack –l 进程号 > tomcat.stack

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

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

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

 

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

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

 

 

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

先把heap信息导出来:

jmap –dump:format=b,file=tomcat.bin tomcat进程号,一共有20G…

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

 

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

    vim MemoryAnalyzer.ini  一般将-Xmx修改为文件大小的两倍

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

 

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

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

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

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

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

 

 

dump文件分析介绍

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

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

 

Dump文件结构大致如下:

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

    可以看到一个JVM进行在运行时有很多个部分在一起协同合作,主要关心我们自己编写的程序部分的堆栈信息就可以了,就是上面的JAVA EE部分。(具体信息详见“参考3”,忽略上面的weblogic.Server.main部分,那是参考文章作者自己的内容)。

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

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

    为什么会有两种状态呢?因为JVM是跨平台的,Linux下有Linux下的状态,Windows下有Windows下的状态,JVM为了方便表示将它们统一到JVM定义的六种状态中来(在java.lang.Thread.State类中)。接下来分别细说下JVM线程状态。

 

 

JVM线程运行状态:

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

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介绍:

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

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

 

 

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

BLOCKED状态:

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

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

 

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

    一个请求过程中多次dump,比如runnable线程,如果一直在执行同一个方法,说明可能有问题了(while死循环)

    看我之前那篇定位CPU高的博客《Linux CPU飙升到了100%怎么排查》

 

CPU占用不高,响应慢:

    Dump多次

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

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

或者直接就是两者很多

 

死锁:

    死锁变现为程序不再响应用户请求或者程序的停顿,dump文件中显示有deadlock或者有两个线程在相互wait(这种我还没遇到过,但是有大神遇到了,截个图分别以后查看)

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

 

 

参考:

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文件一些典型案例)