Shiro在Tomcat下内存泄漏问题

警告 [main] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [SessionValidationThread-1] but has failed to stop it. This is very likely to create a memory leak.

关闭Tomcat后报错:

1
2
3
4
5
6
7
8
9
10
org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [SessionValidationThread-1] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
sun.misc.Unsafe.park(Native Method)
java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1093)
java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
java.lang.Thread.run(Thread.java:748)

1.首先看看org.apache.catalina这个包是谁的?用来干嘛的?

catalina就是Tomcat服务器使用的Apache实现的servlet容器的名字。

2.clearReferencesThreads是用来干嘛的?

这是tomcat关闭应用时检测到了应用启动的线程未被终止,tomcat为防止造成内存泄露,给出警告,并根据配置

来决定是否强制停止该线程(默认不会强制停止)。

这样一来问题的原因就清楚了,不就是关闭Tomcat时有个线程没有终止导致的内存泄漏嘛。

接下来就是找出未关闭线程所属的Class并想办法让他在Tomcat停止前结束就解决了。

1.用jps找出应用进程的pid

jps -l

jps

定位到pid为6168的应用

2.用jstack导出线程dump(Windows可以直接用jvisualvm图形化界面)

jstack 6168 > D:/dump.txt

1
2
3
4
5
6
7
8
9
10
11
12
"SessionValidationThread-1" #63 daemon prio=5 os_prio=0 tid=0x000000001fe10800 nid=0x974 waiting on condition [0x000000000107e000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x0000000774472b78> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1093)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)

掌握到以下信息:是个守护线程,是个定时任务。下面找出是谁创建的SessionValidationThread为前缀的线程就行了,直接在IDEA中全局搜索关键字SessionValidationThread(我这里没下载Maven依赖的Source,所以搜的类名)

class

ExecutorServiceSessionValidationScheduler这个一看就像是了

进去后看到,只截取关键部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class ExecutorServiceSessionValidationScheduler implements SessionValidationScheduler, Runnable {

......

public void enableSessionValidation() {
if (this.interval > 0L) {
this.service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);

public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName(ExecutorServiceSessionValidationScheduler.this.threadNamePrefix + this.count.getAndIncrement());
return thread;
}
});
this.service.scheduleAtFixedRate(this, this.interval, this.interval, TimeUnit.MILLISECONDS);
}

this.enabled = true;
}

public void run() {
if (log.isDebugEnabled()) {
log.debug("Executing session validation...");
}

long startTime = System.currentTimeMillis();
this.sessionManager.validateSessions();
long stopTime = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug("Session validation completed successfully in " + (stopTime - startTime) + " milliseconds.");
}

}

public void disableSessionValidation() {
if (this.service != null) {
this.service.shutdownNow();
}

this.enabled = false;
}

......

}

现在看怎么关闭这个线程,ExecutorServiceSessionValidationScheduler自带销毁的方法,但是怎么调用到他呢?

再来找找ExecutorServiceSessionValidationScheduler是从哪被调用的,这个类是Shiro用作session的定时校验的,我们去Shiri的xml配置中看看session相关配置。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionDAO" ref="redisSessionDAO" />
<!--去掉URL中的JSESSIONID-->
<property name="sessionIdUrlRewritingEnabled" value="false"/>
<!-- 设置超时时间,这里是毫秒 -->
<property name="globalSessionTimeout" value="1800000"/>
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionIdCookieEnabled" value="true"/>
<property name="sessionIdCookie" ref="simpleCookie"/>
</bean>

很清楚了吧,sessionValidationSchedulerEnabled设置为true了,因为DefaultWebSessionManager是在Spring的IOC里的,所以找到里边的销毁方法就好了。但是DefaultWebSessionManager的类继承的有点多,为了方便就Debug吧。

直接在ExecutorServiceSessionValidationScheduler的enableSessionValidation方法上下断点,追踪下链路。

找到了,AbstractValidatingSessionManager类中的enableSessionValidation方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected synchronized void enableSessionValidation() {
SessionValidationScheduler scheduler = this.getSessionValidationScheduler();
if (scheduler == null) {
scheduler = this.createSessionValidationScheduler();
this.setSessionValidationScheduler(scheduler);
}

if (!scheduler.isEnabled()) {
if (log.isInfoEnabled()) {
log.info("Enabling session validation scheduler...");
}

scheduler.enableSessionValidation();
this.afterSessionValidationEnabled();
}

}

同时也找到了提供的销毁方法

1
2
3
public void destroy() {
this.disableSessionValidation();
}

他会最终调用ExecutorServiceSessionValidationScheduler的disableSessionValidation方法,対线程执行shutdownNow()操作

最终解决方法:

新建类MyApplicationListener,监听Spring容器关闭(其他的钩子也行,只要能调用到就OK)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package cn.qinshuang;

import org.apache.shiro.session.mgt.AbstractValidatingSessionManager;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;

public class MyApplicationListener implements ApplicationListener<ApplicationEvent>, ApplicationContextAware {
private ApplicationContext applicationContext;

@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
if (applicationEvent instanceof ContextClosedEvent) {
AbstractValidatingSessionManager s = (AbstractValidatingSessionManager) applicationContext.getBean("sessionManager");
if (s != null) {
s.destroy();
}
}
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

至此,Tomcat正常关闭,控制台也输出了session validation停止的log

[main] INFO o.a.s.s.m.AbstractValidatingSessionManager - Disabled session validation scheduler.

文章作者: Shawn Qin
文章链接: https://qinshuang1998.github.io/2019/09/23/shiro-tomcat-memory-leak/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Shawn's Blog