java: web应用中不经意的内存泄露
前面有一篇讲解如何在spring mvc web应用中一启动就执行某些逻辑,今天无意发现如果使用不当,很容易引起内存泄露,测试代码如下:
1、定义一个类App
package com.cnblogs.yjmyzz.web.controller;
import java.util.Date;
public class App {
boolean isRun = false;
public App() {<br/>
isRun = true;<br/>
}
public void start() {<br/>
while (isRun) {<br/>
System.out.println("=======> I AM ALIVE =>" + new Date());<br/>
try {<br/>
Thread.sleep(1000);<br/>
} catch (InterruptedException e) {<br/>
e.printStackTrace();<br/>
}<br/>
}<br/>
}
public void stop() {<br/>
isRun = false;<br/>
}
}
代码里面的内容不是重点,只是示意一下,我打算在spring mvc 应用一启动时,就让这个类实例化,执行其中的start方法,即:每隔一秒输出一句话。
2、定义一个Listener
import com.cnblogs.yjmyzz.web.controller.App;<br/>
import org.springframework.context.ApplicationListener;<br/>
import org.springframework.context.event.ContextRefreshedEvent;<br/>
import org.springframework.stereotype.Component;
@Component<br/>
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
App app;
@Override<br/>
public void onApplicationEvent(ContextRefreshedEvent evt) {<br/>
if (evt.getApplicationContext().getParent() == null) {
new Thread(new Runnable() {<br/>
@Override<br/>
public void run() {<br/>
app = new App();<br/>
app.start();<br/>
}<br/>
}).start();<br/>
}
}<br/>
}
代码也很简单,应用一启动,就开一个线程,实例化App,然后调用app.start()方法,运行一下,也跟预期的一样,每隔一秒输出类似下面的内容:
=======> I AM ALIVE =>Wed Sep 16 21:55:42 CST 2015
正式部署到jboss上以后,问题来了,在jboss管理控制台上,把这个应用给disable甚至remove后,日志里仍然不断有上面的类似输出,即app的实例仍然活着,其start方法也始终在运行,换句话说,app并没有被销毁。
简单分析一下:jboss的每个server启动后,会伴随启动一个jvm实例,而部署在该server上的web应用,里面创建的各种资源也在这个jvm实例中,就算把应用给停掉甚至删除,由于代码中没有任何清除app或停止start方法的处理,所以这个实例一直存在,不会被销毁,除非server重启。
另一个问题:如果把上面这段代码中,创建线程的部分去掉,改成直接 app = new App(); app.start(); 部署时会发现另一个现象,日志里仍然不断有输出,即代码在执行,但是该应用在jboss中的状态始终是isdeploying,部署一直无法结束,始终处于『部署中』的状态。
原因:start方法中的Thread.sleep()方法会阻塞线程,导致部署无法执行完毕。
解决办法:
import org.springframework.stereotype.Component;<br/>
import javax.annotation.PostConstruct;<br/>
import javax.annotation.PreDestroy;<br/>
import java.util.Date;
@Component<br/>
public class App {
boolean isRun = false;
@PostConstruct<br/>
public void init() {<br/>
System.out.println("init ==> " + new Date());<br/>
isRun = true;<br/>
}
public void start() {<br/>
while (isRun) {<br/>
System.out.println("=======> I AM ALIVE =>" + new Date());<br/>
try {<br/>
Thread.sleep(1000);<br/>
} catch (InterruptedException e) {<br/>
e.printStackTrace();<br/>
}<br/>
}<br/>
}
public void stop() {<br/>
isRun = false;<br/>
}
@PreDestroy<br/>
public void destroy() {<br/>
System.out.println("destroy ==> " + new Date());<br/>
stop();<br/>
}<br/>
}
这里做了几处改进:
a) 加上@Component后,App的实例将由Spring容器自动创建,即由容器来管理
b) 加上了@PreDestroy,Bean的生命周期由Spring容器来管理后,凡是Bean里加上该注解的方法,会在Bean销毁前被执行,通常该方法用于清理资源
c) 将初始化的工作,移到了init方法中,并通过@PostConstruct注解告诉Spring,在调用完Bean的默认构造方法后,自动来调用该方法(当然这一步是可选的,并非必须)
@Component<br/>
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired<br/>
App app;
@Override<br/>
public void onApplicationEvent(ContextRefreshedEvent evt) {<br/>
if (evt.getApplicationContext().getParent() == null) {<br/>
new Thread(new Runnable() {<br/>
@Override<br/>
public void run() {<br/>
app.start();<br/>
}<br/>
}).start();<br/>
}
}
}
Listener中就简单多了,直接@Autowired注入app实例就行了。
个人建议:
a) 如果要在web 应用一启动时,就执行某些操作,特别是对资源类的长连接实例创建(比如:加载数据到缓存中预热、连接到Zookeeper监控节点变化、连接到Ftp准备取数据),最好交给Spring容器来自动创建,且务必记得在Destroy前,清理资源(即:断开连接)
