您的位置:首页 > 运维架构 > Tomcat

类加载器内存泄露与tomcat自定义加载器

2015-12-14 10:59 441 查看

1 类加载器内存泄露

每个对象都持有对它的Class引用,也就持有对它的类加载器的引用。相反的,每个类加载器也持有对它加载过的类的引用,保存在Class的静态字段中。如下图所示(图片源自其他博客)



这就意味着如果一个类的加载器发生了内存泄露,那么与之关联的类和所有的静态字段都发生了泄漏。这意味着一些缓存状态、单例以及配置和状态信息也就暴露了出去。即使你的程序没有保存这些信息,但并不意味着你使用的框架没有,因为他们的jar包也在server 的classpath中,由此来看这是非常危险的。

2 Tomcat的类加载模型

对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是
Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

2.1 目录合并

以Tomcat8为例,其类加载器的结构图如下所示,这与网上大多数帖子的图稍有差异,其实那是Tomcat5的结构图,在那个图示中,Tomcat定义了CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader,他们分别负责加载Tomcat目录下的/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中的Java类库。而在Tomcat6以后/common/*、/server/*、/shared/*三个文件夹合并为/lib目录。

2.2 Tomcat类加载器

Bootstrap :这个Bootstrap是一类加载器的统称,由JVM提供的用来加载Java的jar包
System:简单的说,这个加载器的主要作用是用于加载$CATALINA_HOME/bin/目录下的三个jar包:bootstrap.jar、tomcat-juli.jar和commons-daemon.jar。由它的位置也可以知道,通过这个classloader加载的所有类,都对tomcat自身的类,以及所有web应用的类可见
Common:用来加载tomcat自身的类的加载器。默认加载$CATALINA_HOME/lib/目录下的jar包或为打包的.class文件,而应用程序的依赖jar则不应该放在这个路径下。它的路径定义在$CATALINA_HOME/conf/catalina.properties文件中common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
WebappX :应用程序专属类加载器。Tomcat为每一个部署的Web应用创建一个类加载器,它用于加载/WEB-INF/lib和/WEB-INF/classes内的类文件,它仅对于自己的应用是可见的,而对其他部署在这个Tomcat实例上的Web应用是隔离的。

2.3 tomcat为什么要自定义类加载器

对于Tomcat来说,我认为最重要的就是为每个应用都分配了一个专属类加载器,主要有以下原因:
1 为了不同webapp加载不同版本的jar包:在现在的web应用中,第三方框架的使用随处可见,但是如果两个webapp都使用了同一个jar包但是版本不同,那么就非常有必要进行隔离。我们知道一个classloader实例对同一个class文件仅能加载一次,在加载某class文件一个版本之后,如果再次搜索到同名的class文件是会抛出异常的。这样对于不同版本的jar包加载还是隔离开比较好。
2 当然是为了安全:由本人第一段可以看到,类加载器的内存泄露是致命的。而如果隔离开,则可以保证其他webapp是安全的
3 类的热部署:类加载器之所以能发展到今天,它能实现类的热部署才是王道。对于一个应用来说,如果在不用停机的情况下就可以进行升级显然是最完美的方式。在我们部署一个应用的时候,通常都会设置reload=true。它的实现就是靠类加载器。

3 Tomcat类加载器启动过程

下面这张图基本描述了Tomcat的启动过程



3.1 bootstrap入口

tomcat的启动入口在org.apache.catalina.startup.Bootstrap类中,其main方法如下(仅列出与类加载器相关的逻辑):

(1)Main方法 它是整个Tomcat启动的入口
<div style="text-align: justify;"><span style="font-family: 宋体;">         Bootstrap bootstrap = new Bootstrap();</span></div>            try {
<div style="text-align: justify;"><span style="font-family: 宋体;">            } catch (Throwable t) {</span></div>                bootstrap.init();//初始化入口
<div style="text-align: justify;"><span style="font-family: 宋体;">                t.printStackTrace();</span></div>                handleThrowable(t);
return;
}
<div style="text-align: justify;"><span style="font-family: 宋体;">            daemon = bootstrap;</span></div>

(2)init方法

在init方法里面,对各种类加载器进行了初始化,然后利用catalinaLoader加载了Catalina类并初始化Catalina对象,将Catalina的父加载器设置为sharedLoader。

/** *初始化各种类加载器  利用catalina类加载器加载Catalina对象 */
public void init() throws Exception {

initClassLoaders();//初始化各种类加载器

Thread.currentThread().setContextClassLoader(catalinaLoader);//设置当前线程类加载器为catalinaLoader
SecurityClassLoad.securityClassLoad(catalinaLoader);

// Load our startup class and call its process() method
//这里利用catalinaClassLoader去加载Catalina类 并初始化Catalina对象        if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass =
catalinaLoader.loadClass
// Set the shared extensions class loader           ("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();

if (log.isDebugEnabled())
paramTypes[0] = Class.forName("java.lang.ClassLoader");            log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
Object paramValues[] = new Object[1];
method.invoke(startupInstance, paramValues);//设置Catalina对象的parentClassLoader 为sharedLoader
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);

catalinaDaemon = startupInstance;

}


 

3.2 initClassLoader

在上面的方法中initClassLoaders方法用于初始化各种类加载,现在来看看他初始化了哪些类加载器。
/*** 初始化commonClassLoader serverClassLoader sharedClassLoader	 */
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
<span style="white-space:pre">	</span>  //初始化commonloader
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
catalinaLoader = createClassLoader("server", commonLoader);
//初始化catalinaLoader
commonLoader=this.getClass().getClassLoader();
}
sharedLoader = createClassLoader("shared", commonLoader);//初始化sharedLoader
//创建指定名称的类加载器 并指定其父加载器
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}

/**
*/
//在目前的tomcat默认配置中 仅指定了common.loader="${catalina.base}/lib"
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
//从$CATALINA_HOME/conf/catalina.properties文件中读取对应的classloader的路径
//"${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
<div style="text-align: justify;"><span style="font-family: Arial, sans-serif;">        value = replace(value);</span></div>        String value = CatalinaProperties.getProperty(name + ".loader");
//如果这个值为null,则这个加载器与其父类加载器值相同
//也就是说serverLoader和sharedLoader在默认情况下都是commonLoader
if ((value == null) || (value.equals("")))
return parent;

List<Repository> repositories = new ArrayList<>();

<div style="text-align: justify;"><span style="font-family: Arial, sans-serif;">                        new Repository(repository, RepositoryType.URL));</span></div>        String[] repositoryPaths = getPaths(value);

for (String repository : repositoryPaths) {
// Check for a JAR URL repository
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(
continue;
new Repository(repository, RepositoryType.GLOB));
} catch (MalformedURLException e) {
// Ignore
}

// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(
return ClassLoaderFactory.createClassLoader(repositories, parent);
else if (repository.endsWith(".jar")) {
repositories.add(
new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(
new Repository(repository, RepositoryType.DIR));
}
}


看过以上两个方法的代码,可以看到这个过程实际上初始化了三个类加载器,commonLoader、sharedLoader和catalinaLoader。而在在目前的tomcat默认配置中仅指定了common.loader的值,根据这个值可以看出它主要就是载入Tomcat服务器根路径下lib文件夹里面的资源,也就是说在默认情况下,tomcat中的这三个类加载器都为commonLoader。
到目前为止,我们已经完成了tomcat初始化的前期工作,初始化了catalina对象然后设置了以下关系
(1)Thread.currentThread().setContextClassLoader   设置为catalinaLoader
(2) catalina.setParent()  设置为sharedLoader

3.3 start方法

完成了一些初始化工作之后,tomcat就要调用catalina的start方法来启动。这里仅贴出与类加载器相关的一些代码
(1)在start方法里面,最关键的就是load方法,它主要是为了加载conf/server.xml文件,并生成server对象。然后调用server.start方法完成tomcat的启动

<div style="text-align: justify;"><span style="font-family: Arial, sans-serif;">if (getServer() == null) {</span></div><div style="text-align: justify;"><span style="font-family: Arial, sans-serif;">            load();//加载server.xml文件 生成server对象</span></div>        }

if (getServer() == null) {
<div style="text-align: justify;"><span style="font-family: Arial, sans-serif;">            log.fatal("Cannot start server. Server instance is not configured.");</span></div>            return;
}

long t1 = System.nanoTime();

// Start the new server
try {
<div style="text-align: justify;"><span style="font-family: Arial, sans-serif;">        }</span></div><div style="text-align: justify;"><span style="font-family: Arial, sans-serif;">            getServer().start();//启动最终的tomcat服务器 其实server要做的是启动里面的service  </span></div>


(2)在load方法里 ,关键是创建了Digester对象,它负责用它来解析conf/server.xml文件,根据配置的信息来创建相应的server对象。

Digester digester = createStartDigester();


(3)在Digester对象创建过程中,涉及到了classloader相关的部分如下。这里设置了Digester对象在创建对象的时候使用当前线程contextClassLoader。再结合上面看到,这个classLoader实际就是catalinaLoader。也就是说Server对象都是由catalinaLoader创建的。

digester.setUseContextClassLoader(true);  //将useContextClassLoader参数设置为true,那么待会将会用预先保存的线程classLoader来载入class,这里其实就是catalinaloader


(4)在Digester对象创建中还有关于Engine的一条规则,也就是将Engine的ClassLoader设置为parentClassLoader,而在前面看到 parentClassLoader是sharedLoader,也就是说Engine的parentClassLoader也会是sharedLoader。

(5) 在Digester对象创建中还有关于Host的一条规则,这里将Host的parentClassLoader设置为engine的parentClassLoader,那么Host的parentClassLoader 也同样是sharedLoader

<span style="white-space:pre">	</span>//创建host对象
digester.addObjectCreate(prefix + "Host", "org.apache.catalina.core.StandardHost", "className");
digester.addSetProperties(prefix + "Host");<span style="font-family: Arial, sans-serif;">//创建host对象的配置</span>
digester.addRule(prefix + "Host",new CopyParentClassLoaderRule());  //会将host的parentClassloader设置为engine的,engine被设置为sharedloader


到现在为止,我们再对tomcat的各种对象的classloader进行一次梳理

Catalina 对象的parentClassLoader  :sharedLoader

Engine   对象的parentClassLoader  :sharedLoader

Host       对象的parentClassLoader  :sharedLoader

3.4Context

在前面的Engine Host对象都创建完成之后,最重要就是要创建Context对象。Context对象的启动是在StandardContext类的startInternal方法中。这个方法中与类加载器相关的部分如下:

if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}


(1)从这可以看到初始化了一个webappLoader,并设置为当前的Context的classloader。而这里创建的webapploader指定了它的parentLoader为当前context的parentloader,我们知道context上一层就是Host,也就是说webapploader的parentClassloader是sharedLoader。

(2) 下面进入到webapploader的startInternal方法。在这里面有一行代码创建了classLoader,这里实际上创建的是一个WebappClassLoaderBase对象。

classLoader = createClassLoader();//

//创建webappclassLoader,这里会将sharedLoader设置为parent
private WebappClassLoader createClassLoader()
throws Exception {

Class<?> clazz = Class.forName(loaderClass);  //获取要创建的classLoader的class引用  org.apache.catalina.loader.WebappClassLoader
WebappClassLoader classLoader = null;

if (parentClassLoader == null) {
parentClassLoader = context.getParentClassLoader();    //获取context的parentClassLoader,这里是sharedLoader
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);  //获取构造函数,这里需要传递一个parentClassLoader,其实这里的双亲loader就是sharedLoader
classLoader = (WebappClassLoader) constr.newInstance(args);

return classLoader;
}


(3)在webapploader的startInternal方法中有一句调用WebappClassLoader的start方法,在这个方法里就有我们非常熟悉的/WEB-INF/classes和/WEB-INF/lib啦。

public void start() throws LifecycleException {

WebResource classes = resources.getResource("/WEB-INF/classes");  //获取/WEB-INF/classes目录的资源引用
if (classes.isDirectory() && classes.canRead()) {
addURL(classes.getURL());   //将该资源添加到当前classLoader的资源库
}
WebResource[] jars = resources.listResources("/WEB-INF/lib");  //这里是获取lib文件夹
for (WebResource jar : jars) {  //遍历所有的资源
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
addURL(jar.getURL());  // 将资源加入到classLoader的资源库
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}

started = true;
String encoding = null;
try {
encoding = System.getProperty("file.encoding");
} catch (SecurityException e) {
return;
}
if (encoding.indexOf("EBCDIC")!=-1) {
needConvert = true;
}

}


到这为止,就浏览了tomcat启动过程中与classLoader有关的代码。再回头去看看最开始那张结构图,实际上我们在讲common及以下的部分。在Tomcat6以后,尽管在目录下已经找不到shared、common、server这样的文件夹,但是在源码中,这样的结构并没有改变,只是默认情况下他们都是common。

3.5 StandardWrap

对于一个web应用来说,我们实现功能的地方无非是在Servlet、Listener、Filter中,那么这三个是如何构造出来的呢?他们实际对应的是StandardWrapper、ApplicationListener、FilterDef、FilterMap的实例。而他们的构造全部是在org.apache.catalina.core.StandardContext类的startInternal方法中,下面以Servlet为例,在startInternal方法中有一个loadOnStartup方法,它是负责加载所有的"load
on startup" Servlet,而真正实现servlet加载的是在StandardWrapper中的load方法:

InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
try {
servlet = (Servlet) instanceManager.newInstance(servletClass);
}

这里仅列出该方法关键的语句。可以看到实际上一个Servlet的初始化是由instaneManager实现的。而这个instaneManager是由context持有的,现在来猜想一下,instaneManager里一定持有了某个classLoader来加载Servlet类。

下面来认证我们的想法,每一个context中都持有一个DefaultInstanceManager实例,在这里面定义了newInstance方法:

public Object newInstance(String className) throws IllegalAccessException,
InvocationTargetException, NamingException, InstantiationException,
ClassNotFoundException {
Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
return newInstance(clazz.newInstance(), clazz);
}


这里面我们看到了classLoader,那么这个classLoader到底是谁呢?在DefaultInstanceManager的构造方法里面,看到了它的身影

classLoader = catalinaContext.getLoader().getClassLoader();


这面的catalinaContext的就是前面的3.4节提到的context,那么这个classLoader也就是webappClassLoader了

3.6 示例验证

这里来写一个简单的servlet ,然后获取它的全部classLoader

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(this.getClass().getClassLoader());

java.lang.ClassLoader classLoader=this.getClass().getClassLoader();
System.out.println("=============");

while(classLoader!=null)
{
System.out.println("加载器:"+classLoader.getClass().getCanonicalName());
classLoader=classLoader.getParent();
}

}


输出的结果为:
WebappClassLoader
context: WelcomPage
delegate: false
----------> Parent Classloader:
java.net.URLClassLoader@2626b418

=============
加载器:org.apache.catalina.loader.WebappClassLoader
加载器:java.net.URLClassLoader
加载器:sun.misc.Launcher.AppClassLoader
加载器:sun.misc.Launcher.ExtClassLoader


这里面显然WebappClassLoader重写了toString方法,所以我们利用getClass().getCanonicalName()方式类获取classLoader的类名。

第一个输出说明servlet的classLoader是WebappClassLoader,

第二个输出是一个URLClassLoader的对象,按照我们前面所知,它应该是sharedLoader才对啊?sharedLoader又是什么呢?默认情况下是commonLoader。难道commonLoader是URLClassLoader吗?没错就是这样的。在Bootstrap的createClassLoader方法中,利用ClassLoaderFactory类的createClassLoader方法来创建classLoader,而这个方法实际上就是实例化了一个URLClassLoader。

return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});

第三行第四行的输出就不用解释了,他们显然是JVM的类加载器。没有他们哪有Java运行环境呢?

现在再看看上面的流程图,是不是就完全理解了?

4 Tomcat类的热部署

在用tomcat进行开发的时候,我们都有这样的经历,启动着tomcat的时候修改web工程内的源代码,很短时间内就会自动加载到web工程内,而并不需要重新启动tomcat。这就是热部署,那么Tomcat是如何实现的呢?在StandardContext里面运行着一个backgroundProcess,它实际去调用webappClassloader的backgroundProcess方法

public void backgroundProcess() {
if (reloadable && modified()) {//检测是否有改变并且当前应用是否允许重新加载
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (context != null) {
context.reload();//重新加载
}
} finally {
if (context != null && context.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(context.getLoader().getClassLoader());
}
}
}
}

这里面的reload方法实际上就是就是重复前面3.4里面的startInternal过程。

而前面提到的modified方法就是负责去检查"/WEB-INF/lib"和/WEB-INF/classes是否有修改

 

/**
* 检查/WBE-INF/lib和/WEB-INF/classes路径是否发生改变
*/
public boolean modified() {

if (log.isDebugEnabled())
log.debug("modified()");

for (Entry<String,ResourceEntry> entry : resourceEntries.entrySet()) {
long cachedLastModified = entry.getValue().lastModified;//获取文件的最后一次修改时间
long lastModified = resources.getClassLoaderResource(
entry.getKey()).getLastModified();
if (lastModified != cachedLastModified) {//发生了修改
if( log.isDebugEnabled() )
log.debug(sm.getString("webappClassLoader.resourceModified",
entry.getKey(),
new Date(cachedLastModified),
new Date(lastModified)));
return true;
}
}

// Check if JARs have been added or removed
WebResource[] jars = resources.listResources("/WEB-INF/lib");//检查jar包
// Filter out non-JAR resources

int jarCount = 0;
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
jarCount++;
Long recordedLastModified = jarModificationTimes.get(jar.getName());
if (recordedLastModified == null) {//如果在缓存中没有找到个jar的相关信息  证明是新加的jar包
// Jar has been added
log.info(sm.getString("webappClassLoader.jarsAdded",
resources.getContext().getName()));
return true;
}
if (recordedLastModified.longValue() != jar.getLastModified()) {//最后一次修改时间发生了改变
// Jar has been changed
log.info(sm.getString("webappClassLoader.jarsModified",
resources.getContext().getName()));
return true;
}
}
}

if (jarCount < jarModificationTimes.size()){
log.info(sm.getString("webappClassLoader.jarsRemoved",
resources.getContext().getName()));
return true;
}

// No classes have been modified
return false;
}




内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息