0.前言 其实从去年年底接到Spring
项目,每次在IDEA关闭项目时,都会在控制台报出Tomcat
内存溢出的异常,但是暂时看不出任何影响,一直是将就用着。但是,我这个人一直都强迫症,看着一个红色的警告就不爽,就一定要想办法解决的。最终,通过各种努力,暂时解决的这些警告了,但不知道未来的开发和部署中,会不会因此出现问题,如果有问题,我再补充这篇文章。
1.开发环境
Oracle JDK 1.8
Tomcat 8.0
MySQL 5.7
2.异常及解决方案 以下多个异常,有几个是我亲历并解决的,c3p0连接池链接未关闭的异常可能以前学长学姐们遇到过,项目配置中早已解决,只是顺道提出。
2.1.MySQL JDBC未注销异常 控制台的异常信息如下:1
2
30-Mar-2019 13:49:36.807 警告 [localhost-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesJdbc The web application [ROOT] registered the JDBC driver [com.alibaba.druid.proxy.DruidDriver] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
30-Mar-2019 13:49:36.808 警告 [localhost-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesJdbc The web application [ROOT] registered the JDBC driver [com.mysql.jdbc.Driver] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
参考 leeyom 在文章Tomcat关闭报内存溢出异解决方案 里所说:
在tomcat 6.0.24
版本之后,加入了一个memory leak listener
(JreMemoryLeakPreventionListener,有兴趣可详细查去源码),在tomcat stop
、undeployed
、reloaded
的时候,他会检测当前应用的classloader
,查看是否有引用泄露。
Tomcat定义了一系列的引用泄露规则:
threadlocal保持引用
线程池保持引用
驱动注册
如有引用泄露,则提示错误,例如this is very likely to create a memory leak
类似这样的错误。对于The web application [LeadermentEnterpriseSystemV2] registered the JDBC driver [com.mysql.jdbc.Driver] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
这种异常其实就是MySQL
的JDBC
驱动无法注销的原因所造成的。
于是,我参考其方法二,创建了一个
CustomServletContextListener
,然后在 contextDestroyed 方法中手动注销。具体如下:
这样的话就可以解决MySQL驱动无法注销的问题。
2.2.Shiro权限框架会话验证调度器Scheduler未Shutdown异常 这个问题是因为Shiro
权限框架里有用到Quartz
,所以出现了以下10个一样的警告:1
2
3
4
5
30-Mar-2019 13:49:36.810 警告 [localhost-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [DefaultQuartzScheduler_Worker-1] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
java.lang.Object.wait(Native Method)
org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:519)
……
(此处省略9个除DefaultQuartzScheduler_Worker编号不同以外的相同警告)
Tomcat关闭报内存溢出异解决方案 里说,可以重新写了个过滤器,去判定session是否过期,而不采用shiro的会话验证器。而一般配置里采用的是如下的会话验证器:1
2
3
4
<bean id ="sessionValidationScheduler" class ="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler" >
<property name ="sessionValidationInterval" value ="1800000" />
<property name ="sessionManager" ref ="sessionManager" />
</bean >
因为我尚未做这部分功能,也没有找到比较好的重写教程,因此没有采用这种解决方式。在直接参看Spring
与Quartz
相关资料时,我发现不少人认为 Quartz 为开发者做了过多的事,反而导致开发的麻烦,其中常见的就是内存泄漏,显然 Shiro 也存在这个问题。 当我看到Spring3.2.11与Quartz2.2.1整合时内存泄漏的问题的解决 中所写到的:1
2
3
4
究其原因是开启的scheduler_Worker线程没有关闭。但Spring的SchedulerFactoryBean实现了DisposableBean接口,表示web容器关闭时会执行destroy()
……(省略源码部分)
表示工厂已经做了shutdown。所以,问题出现在真正执行的scheduler.shutdown(true)。
原来这是Quartz的bug(见https://jira.terracotta.org/jira/browse/QTZ-192),在调用scheduler.shutdown(true)后,Quartz检查线程并没有等待那些worker线程被停止就结束了。
这时,我想到能否手动通过这种方式去shutdown那些worker线程,于是我翻看了org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler
源码,发现其有一个受保护的方法protected Scheduler getScheduler()
可以获取到Scheduler,且其他所有方法都是public的,也只有一个空构造函数:1
2
3
4
5
6
7
protected Scheduler getScheduler () throws SchedulerException {
if (this .scheduler == null ) {
this .scheduler = StdSchedulerFactory.getDefaultScheduler();
this .schedulerImplicitlyCreated = true ;
}
return this .scheduler;
}
于是,我决定尝试继承org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler
类,并实现DisposableBean
接口,让它在Servlet生命周期destroy时,自动注销:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CustomQuartzSessionValidationScheduler extends QuartzSessionValidationScheduler implements DisposableBean {
public CustomQuartzSessionValidationScheduler () {
}
public CustomQuartzSessionValidationScheduler (ValidatingSessionManager sessionManager) {
super (sessionManager);
}
@Override
public void destroy () throws Exception {
Scheduler scheduler = this .getScheduler();
scheduler.shutdown(true );
try {
Thread.sleep(1000 );
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
最后替换之前的bean对象,再次关闭Tomcat时,不会再出现之前的警告。另外我也想说明的是,我暂时不确定这个方法是有效的,但是暂时不会出现OOM错误了。配置文件:1
2
3
4
<bean id ="sessionValidationScheduler" class ="com.jiacyer.test.CustomQuartzSessionValidationScheduler" >
<property name ="sessionValidationInterval" value ="1800000" />
<property name ="sessionManager" ref ="sessionManager" />
</bean >
2.3.c3p0连接池链接未关闭 修改applicationContext.xml文件,添加属性 destroy-method=”close”,即可解决数据库连接未关闭的问题。1
2
3
4
5
6
7
8
9
10
11
<bean id ="dataSource" class ="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method ="close" >
<property name ="user" value ="${jdbc.user}" > </property >
<property name ="password" value ="${jdbc.password}" > </property >
<property name ="jdbcUrl" value ="${jdbc.jdbcUrl}" > </property >
<property name ="driverClass" value ="${jdbc.driverClass}" > </property >
<property name ="idleConnectionTestPeriod" value ="60" />
<property name ="maxIdleTime" value ="60" />
<property name ="testConnectionOnCheckin" value ="false" />
<property name ="testConnectionOnCheckout" value ="true" />
<property name ="preferredTestQuery" value ="SELECT 1" />
</bean >
2.4.自定义定时任务连接池链接未关闭 项目中不少地方需要用到定时任务,于是我想把所有的定时任务统一放在一个线程池里运行,方便统一控制,于是我写了如下TimerUtils
类,关于Executor
的相关知识,可以参看Executor 之 线程池及定时器 :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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class TimerUtils {
private static final ThreadLocal<ScheduledExecutorService> scheduledExecutorService = new ThreadLocal<ScheduledExecutorService>() {
@Override
protected ScheduledExecutorService initialValue () {
return Executors.newScheduledThreadPool(10 );
}
};
public static ScheduledExecutorService getInstance () {
return scheduledExecutorService.get();
}
public static void schedule (Runnable command, long delay, TimeUnit timeUnit) {
scheduledExecutorService.get().schedule(command, delay, timeUnit);
}
public static void schedule (Runnable command, long startTime, long period) {
startTime -= System.currentTimeMillis();
scheduledExecutorService.get().scheduleAtFixedRate(command, startTime, period, TimeUnit.MILLISECONDS);
}
public static void scheduleAtFixedRate (Runnable command, long initialDelay, long period, TimeUnit timeUnit) {
scheduledExecutorService.get().scheduleAtFixedRate(command, initialDelay, period, timeUnit);
}
public static void scheduleWithFixedDelay (Runnable command, long initialDelay, long delay, TimeUnit timeUnit) {
scheduledExecutorService.get().scheduleWithFixedDelay(command, initialDelay, delay, timeUnit);
}
public static boolean shutdown () {
boolean b = false ;
ScheduledExecutorService service = scheduledExecutorService.get();
if (!service.isShutdown()) {
try {
service.shutdown();
b = service.awaitTermination(10 , TimeUnit.SECONDS);
if (!b)
service.shutdownNow();
} catch (Exception e) {
e.printStackTrace();
}
}
return b;
}
}
但这时,无论我在哪里调用shutdown方法,似乎都无法停止线程池的所有线程,导致Tomcat无法正常关闭,后台始终有Java VM在运行。其原因正如在java web项目中慎用Executors以及非守护线程 所说:1
一般情况下,我们使用executors创建多线程时,就会使用默认的threadFactory(即调用只有一个参数的工厂方法),而创建出来的线程就是非守护的。而相应的程序就永远不会退出,如采用Executors创建定时调度任务时,这个调试任务永远不会退出。
因此,解决的办法就是重写相对应的threadFactory
,将TimerUtils
类的静态变量scheduledExecutorService
更改为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final ThreadLocal<ScheduledExecutorService> scheduledExecutorService = new ThreadLocal<ScheduledExecutorService>() {
@Override
protected ScheduledExecutorService initialValue () {
return Executors.newScheduledThreadPool(10 , new ThreadFactory() {
@Override
public Thread newThread (Runnable r) {
Thread thread = Executors.defaultThreadFactory().newThread(r);
thread.setDaemon(true );
return thread;
}
});
}
};
并且,在CustomServletContextListener
类的public void contextDestroyed(ServletContextEvent sce)
方法中调用TimerUtils.shutdown()
。此时,Tomcat就可以正常关闭了。
3.结语 最终的原因归根结底就是Tomcat的进程无法释放的问题,每次问题的出现,都只有通过一步步的排查才能根本上解决。虽然很多知识尚需学习,以上问题的解决方案也未必不会干扰到以后的开发,但是至少暂时Tomcat停止的时候,不在报内存溢出的警告了。如果有更好的解决方案或者任何疑问,可以在评论区留言一起讨论。
参考贴来源:Tomcat关闭报内存溢出异解决方案 作者:Leeyom Spring3.2.11与Quartz2.2.1整合时内存泄漏的问题的解决 作者:Yancey.Han 在java web项目中慎用Executors以及非守护线程 作者:折腾数据折腾代码 Executor 之 线程池及定时器
转载请注明出处,无偿提供。