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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010723709/article/details/50291315

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吗?没错就是这样的。BootstrapcreateClassLoader方法中,利用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;
    }




















没有更多推荐了,返回首页