问题表现
应用启动几个小时后,死掉,临死前报错:
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
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) at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach(LinuxDebuggerLocal.java:278) at sun.jvm.hotspot.HotSpotAgent.attachDebugger(HotSpotAgent.java:671) at sun.jvm.hotspot.HotSpotAgent.setupDebuggerLinux(HotSpotAgent.java:611) at sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:337) at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:304) at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:140) at sun.jvm.hotspot.tools.Tool.start(Tool.java:185) at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118) at sun.jvm.hotspot.tools.PMap.main(PMap.java:72) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at sun.tools.jmap.JMap.runTool(JMap.java:201) at sun.tools.jmap.JMap.main(JMap.java:130) Caused by: sun.jvm.hotspot.debugger.DebuggerException: cannot open binary file at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach0(Native Method) at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.access$100(LinuxDebuggerLocal.java:62) at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$1AttachTask.doit(LinuxDebuggerLocal.java:269) at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.run(LinuxDebuggerLocal.java:138)
看到可以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几个工具之间未自动关联,影响排查效率,且缺乏同类线程数量统计的功能。
稍后可开发相应的监控、排查工具,提高同类问题排查效率。