记录一次神奇的内存泄露

今天在维护代码时,在一台长时间没用的服务器上,运行了测试的程序,发现程序运行了一段时间,就报出了内存泄露(如下图),然后导致整个项目停止运行。记录一次神奇的内存泄露
由于改动的比较多,排查比较困难。刚开始以为是它报的ScheduledThreadPoolExecutor相关,因为刚好也改动了这个定时器的线程池相关的配置。然后排查了所有和线程池相关的部位,发现不管是回滚还是修改,都还是会爆出相同的内存泄露的问题。

而后又排查了锁相关的部位,有没有可能有死锁的操作,同样的回滚和修改,都不能解决。

把日志级别改成DEBUG,还是没有任何错误原因。

接着还通过jvm的jvisualvm工具,排查了堆栈的空间,JVM运行的CPU、内存等可能的地方。发现年轻代的S区满了两次,就会爆出内存泄露,一度以为找到了原因。看到程序的char[]数组占用的空间最大,由此考虑有没有可能是String导致的原因。

最后,因为考虑到,内存泄露并不一定会导致整个程序停止运行,所以有没有可能是加载什么的时候出现了问题,加载操作一个是启动的时候,一个是定时器。定时器之前已经排查了,所以重点看了看启动后的加载类,一个实现了ApplicationListener< ContextRefreshedEvent >接口的实现类。这里面有个对字符串非空判断的修改,之前没有非空判断,这次加上了str.length()>0,居!然!只!判断了str的长度大于0,没有判断null。。。简直了。而后干脆使用了lang3中的StringUtils.isBlank()方法。至此问题解决了。


问题解决,但是百思不得其解
为什么明明是空指针异常,却不报空指针,反而爆出了内存泄露呢?
首先,我在 onApplicationEvent方法中,啥也不写,直接抛出一个NullPointerException异常,在程序运行后,直接就报了内存泄露,而没有报空指针异常,所以,可以肯定的是这个接口中,出现异常就会报内存泄露了。而这个相当于程序的启动时抛出的异常,在这里报了异常,说明启动没成功,所以spring就直接结束程序的运行了。
然后我试了试在这个方法中抛出RuntimeException、Exception和Error,发现是Exception的都会报内存泄露错误,而Error则不会。通过断点,找到了这个方法,发现运行时异常和错误都会抛出。更疑惑了,为什么没有抛出呢?
记录一次神奇的内存泄露

后来发现,在执行这个方法之前,就抛出了内存泄露。那就往前找,发现异常是在Stopping service [Tomcat]时抛出的,找到tomcat里的stop方法,发现是WebappClassLoaderBase类的clearReferencesThreads()方法中的下图这个方法抛出的溢出。至此问题明了,程序在onApplicationEvent方法中,抛出了异常,导致程序启动失败。tomcat自动停止,而此时线程池的信息如下:[email protected][Running, pool size = 1, active threads = 0, queued tasks = 1, completed tasks = 0],并没有停止,于是就报出了可能内存泄露的异常。

记录一次神奇的内存泄露

至于为什么没有抛出空指针,是因为此时抛出的异常,被SpringBootExceptionHandler类给捕获了,SpringBootExceptionHandler实现了UncaughtExceptionHandler接口,会把没有捕获的异常,如Error 和 RuntimeException 及其子类给自动捕获,正好就是上面类所抛出的异常,而在SpringBootExceptionHandler这个类的方法内并没有对其输出,所以,实际上异常是被这个类给吃了。。。所以在控制台和日志中并没有发现空指针异常的错误。
最后
UncaughtExceptionHandler是在java 1.5版本出现的 ,当线程由于未捕获异常突然终止时调用的处理程序的接口。