一次线程泄露问题排查笔记

问题表现

应用启动几个小时后,死掉,临死前报错:

    org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread

目前服务重启了,可以预见几个小时后,程序依然会挂。

字面意思是不能创建本地进程,内存满了。可能的原因有两个:一是,本地native内存满了,不足以创建新的线程;二是线程数超出了本地文件描述符(FD,Linux下一切皆文件)限制。

问题可能就出在某几行代码上,有几个小时的反应时间,说明这个业务并发并不高。

排查过程

1. xss设置 、操作系统native内存 、剩余栈内存,及操作系统线程限制

首先,服务器执行Top命令,关注Mem,used和free;关注服务进程RES、%MEM,记下当前量,监控半小时候做比较。

KiB Mem :  7747272 total,  1063088 free,  5605736 used,  1078448 buff/cacheKiB Swap:  4186108 total,  4186108 free,        0 used.  1777504 avail Mem  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM    TIME+ COMMAND  9527 tomcat    20  0 8458412 5.165g  19016 S  5.6 69.9 886:48.03 jsvc          

然后,看xss设置,如果设置过大,每个线程占用内存过多,可创建的线程就少,不了解运维最近是否改过,谨慎起见,看看:

#在服务器执行jps,没看到服务进程

#原因是当前用户没有权限,启动进程的是另外一个用户,找运维su 

#然后jps -v

扫描二维码关注公众号,回复: 3297522 查看本文章

9527  blabla -Xss228k Djava.rmi.server.hostname=1.1.1.1 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=6081 blabla

228K,并不大。

本地执行jmap报错:

Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: cannot open binary file sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: cannot open binary file at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:163) 

看到可以jmx连上去,本地启动jconsole(本地打开控制台,执行D:\Java\jdk1.8.0_51\bin>jconsole.exe,也可以用jvisualvm.exe一样的),连上去看看。

切换到“内存”Tab,关注非堆内存。按228K计算下内存最多能撑住多少个线程。

接下来,看看操作系统线程数限制:

$ ulimit -a
open files (-n) 65535 
max user processes (-u) 4096 

这里限制的4096是processes,而报错里说的是thread,两个有关联,但不是绝对的。一个process可以有多个thread。

#执行如下命令,获得 62375
cat /proc/sys/kernel/threads_max

62375,这个应该是系统线程限制,超大,不会到这个值内存就会爆掉。

2. 找到哪个thread没有释放

从报错看,是有代码一直在开线程,且不关闭线程。类似这样的代码:

while(true){
     new Thread( 
           public void run(){ 
                 while(true);//我的事儿没完没了
          }
     ).run();
}

ps可以查看进程详细信息。
pstree -p 9527|wc -l #可以查看当前java进程的线程数。
jstack可以获取当前线程的详细信息。
jconsole切换到“线程”标签,也可以看线程数量,甚至具体的线程的当前状态。
jvisualvm线程可视化程度更好一点。

这几个工具结合用,基本可以找到问题了。

通过ps p 26683 -L -o pcpu,pid,tid,time,tname,lstart,wchan > ps.log
可以把线程信息存成文件,仔细看,找到怀疑有问题的进程记下tid,可以通过jstack获得详细进程信息。

ps或pstree中获得的tid需要转成16进制,才可以对应到jstack中的线程号。可以通过printf "%x\n" 21742命令转,得到54ee。

执行jstack 9527|grep 54ee -A 10可以看到进程详细信息。

基本怀疑到了:me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder$IdleConnectionMonitorThread.run(DefaultApacheHttpClientBuilder.java:301)

为了进一步印证,将上面得到ps.log文件,连同jstack 9527 > jstack.log得到文件做关联分析,通过一个小程序,分析文本,按进程名分类统计线程个数(可以自己写一下,很快可写好)。

记下当前时间,过10分钟再统计,发现问题就出现在 IdleConnectionMonitorThread 这个进程上了。一直不断增加。

jconsole看到的进程数曲线也一直在上扬,增加的数量和IdleConnectionMonitorThread增加的数量也基本对上了。

3. 代码分析

相关代码用到了binarywang/weixin-java-common,问题出在了:

ApacheHttpClientBuilder apacheHttpClientBuilder= DefaultApacheHttpClientBuilder.get(); httpClient = apacheHttpClientBuilder.build();

上面代码每次请求都会执行。

binarywang的本意ApacheHttpClientBuilder是单例的,但每次调用get都会创建一个新的ApacheHttpClientBuilder;每个ApacheHttpClientBuilder都会有一个后台进程检测空闲连接是否释放,这个后台连接不断被创建,于是出事了。

具体分析:

分析:

 DefaultApacheHttpClientBuilder.get();的实现:

  public static DefaultApacheHttpClientBuilder get() {
    return new DefaultApacheHttpClientBuilder();
  }

  build的实现:

  public CloseableHttpClient build() {
    if (!prepared.get()) {
      prepare();
    }
    return this.httpClientBuilder.build();
  }

  private final AtomicBoolean prepared = new AtomicBoolean(false);

  如果每个请求都调用 get() --> new DefaultApacheHttpClientBuilder() --> prepared是DefaultApacheHttpClientBuilder的成员变量。

  每个DefaultApacheHttpClientBuilder实例的创建,prepare()都会执行一遍,导致创建prepare中创建了idleConnectionMonitorThread线程:

this.idleConnectionMonitorThread = new IdleConnectionMonitorThread(

      connectionManager, this.idleConnTimeout, this.checkWaitTime);

    this.idleConnectionMonitorThread.setDaemon(true);//事儿干完了会关掉线程

    this.idleConnectionMonitorThread.start();

  每个idleConnectionMonitorThread线程对应一个DefaultApacheHttpClientBuilder实例。

  且未找到 this.idleConnectionMonitorThread 的shutdown()调用(线程关不掉),这个线程会一直存在,不会自动关闭。

4. 问题复现、解决及后续监控

正如分析,立竿见影。

总结

内存oom是表象,关键是线程未及时释放。
ps、jstack、jconsole、jvisualvm几个工具之间未自动关联,影响排查效率,且缺乏同类线程数量统计的功能。

稍后可开发相应的监控、排查工具,提高同类问题排查效率。

猜你喜欢

转载自blog.csdn.net/stationxp/article/details/80664195
今日推荐