ClassLoader解惑

一、什么是Classloader

一個(gè)Java程序要想運(yùn)行起來,首先需要經(jīng)過編譯生成 .class文件,然后創(chuàng)建一個(gè)運(yùn)行環(huán)境(jvm)來加載字節(jié)碼文件到內(nèi)存運(yùn)行,而.class 文件是怎樣被加載中jvm 中的就是Java Classloader所做的事情。

那么.class文件什么時(shí)候會被類加載器加載到j(luò)vm中運(yùn)行那?比如執(zhí)行new操作時(shí)候,當(dāng)我們使用Class.forName("包路徑+類名"),Class.forName("包路徑+類名",classloader),classloader.loadclass("包路徑+類名");時(shí)候就觸發(fā)了類加載器去類加載對應(yīng)的路徑去查找*.class,并創(chuàng)建Class對象。另外理解 cl只能加載jar里面的文件夾里面的class文件,不能加載jar里面嵌套的jar里面的class,這個(gè)很重要

阿里巴巴長期招聘Java研發(fā)工程師p6,p7,p8等上不封頂級別,有意向的可以發(fā)簡歷給我,注明想去的部門和工作地點(diǎn):1064454834@qq.com

二、Java自帶的Classloader

2.1 BootstrapClassloader

引導(dǎo)類加載器,又稱啟動類加載器,是最頂層的類加載器,主要用來加載Java核心類,如rt.jar、resources.jar、charsets.jar等,Sun的JVM中,執(zhí)行java的命令中使用-Xbootclasspath選項(xiàng)或使用- D選項(xiàng)指定sun.boot.class.path系統(tǒng)屬性值可以指定附加的類,它不是 java.lang.ClassLoader的子類,而是由JVM自身實(shí)現(xiàn)的該類c 語言實(shí)現(xiàn),Java程序訪問不到該加載器。通過下面代碼可以查看該加載器加載了哪些jar包

public void test() {  
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();    
        for (int i = 0; i < urls.length; i++) {    
            System.out.println(urls[i].toExternalForm());    
        }   
    }  

執(zhí)行結(jié)果:
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/classes,
寫到這里大家應(yīng)該都知道,我們并沒有在classpath里面指定這些類的路徑,為啥還是能被加載到j(luò)vm并使用起來了吧,因?yàn)檫@些是bootstarp來加載的。

2.2 ExtClassloader

擴(kuò)展類加載器,主要負(fù)責(zé)加載Java的擴(kuò)展類庫,默認(rèn)加載JAVA_HOME/jre/lib/ext/目下的所有jar包或者由java.ext.dirs系統(tǒng)屬性指定的jar包。放入這個(gè)目錄下的jar包對所有AppClassloader都是可見的(后面會知道ExtClassloader是AppClassloader的父加載器)。那么ext都是在那些地方加載類內(nèi):

System.out.println(System.getProperty("java.ext.dirs"));  

/Users/zhuizhumengxiang/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

2.3 AppClassloader

系統(tǒng)類加載器,又稱應(yīng)用加載器,本文說的SystemClassloader和APPClassloader是一個(gè)東西,它負(fù)責(zé)在JVM啟動時(shí),加載來自在命令java中的-classpath或者java.class.path系統(tǒng)屬性或者 CLASSPATH操作系統(tǒng)屬性所指定的JAR類包和類路徑。調(diào)用ClassLoader.getSystemClassLoader()可以獲取該類加載器。如果沒有特別指定,則用戶自定義的任何類加載器都將該類加載器作為它的父加載器,這點(diǎn)通過ClassLoader的無參構(gòu)造函數(shù)可以知道如下:

 protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

執(zhí)行以下代碼即可獲得classpath加載路徑:

System.out.println(System.getProperty("java.class.path"));

2.4 三種加載器聯(lián)系

用一張圖來表示三張圖的關(guān)系如下:


screenshot.png

用戶自定義的無參加載器的父類加載器默認(rèn)是AppClassloader加載器,而AppClassloader加載器的父加載器是ExtClassloader,通過下面代碼可以驗(yàn)證:

ClassLoader.getSystemClassLoader().getParent()

一般我們都認(rèn)為ExtClassloader的父類加載器是BootStarpClassloader,但是其實(shí)他們之間根本是沒有父子關(guān)系的,只是在ExtClassloader找不到要加載類時(shí)候會去委托BootStrap加載器去加載。
通過如下代碼可以知道父加載器為null

ClassLoader.getSystemClassLoader().getParent().getParent()

2.5 類加載器原理

Java類加載器使用的是委托機(jī)制,也就是子類加載器在加載一個(gè)類時(shí)候會讓父類來加載,那么問題來了,為啥使用這種方式那?因?yàn)檫@樣可以避免重復(fù)加載,當(dāng)父親已經(jīng)加載了該類的時(shí)候,就沒有必要子ClassLoader再加載一次。考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時(shí)使用自定義的String來動態(tài)替代java核心api中定義的類型,這樣會存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況,因?yàn)镾tring已經(jīng)在啟動時(shí)就被引導(dǎo)類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠(yuǎn)也無法加載一個(gè)自己寫的String,除非你改變JDK中ClassLoader搜索類的默認(rèn)算法。下面我們從源碼看如何實(shí)現(xiàn)委托機(jī)制:

protected Class<?> loadClass(Stringname,boolean resolve)  
       throws ClassNotFoundException  
   {  
       synchronized (getClassLoadingLock(name)) {  
           // 首先從jvm緩存查找該類
           Class c = findLoadedClass(name);  (1)
           if (c ==null) {  
               longt0 = System.nanoTime();  
               try {  //然后委托給父類加載器進(jìn)行加載
                   if (parent !=null) {  
                       c = parent.loadClass(name,false);  (2)
                   } else {  //如果父類加載器為null,則委托給BootStrap加載器加載
                       c = findBootstrapClassOrNull(name);  (3)
                   }  
               } catch (ClassNotFoundExceptione) {  
                   // ClassNotFoundException thrown if class not found  
                   // from the non-null parent class loader  
               }  
  
               if (c ==null) {  
                   // 若仍然沒有找到則調(diào)用findclass查找
                   // to find the class.  
                   longt1 = System.nanoTime();  
                   c = findClass(name);  (4)
  
                   // this is the defining class loader; record the stats  
                   sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 -t0);  
                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);  
                   sun.misc.PerfCounter.getFindClasses().increment();  
               }  
           }  
           if (resolve) {  
               resolveClass(c);  //鏈接,文件格式,字節(jié)碼驗(yàn)證,符號引用轉(zhuǎn)為直接引用等
           }  
           returnc;  
       }  
   }  

分析代碼知道首先會執(zhí)行(1)從jvm緩存查找該類,如何該類之前被加載過,則直接從jvm緩存返回該類,否者看當(dāng)前類加載器是否有父加載器,如果有的話則委托為父類加載器進(jìn)行加載(2),否者調(diào)用(3)委托為BootStrapClassloader進(jìn)行加載,如果還是沒有找到,則調(diào)用當(dāng)前Classloader的findclass方法進(jìn)行查找。
從上面源碼知道要想修改類加載委托機(jī)制,實(shí)現(xiàn)自己的載入策略 可以通過覆蓋ClassLoader的findClass方法或者覆蓋loadClass方法來實(shí)現(xiàn)。

2.6 Java中如何構(gòu)造三種類加載器的結(jié)構(gòu)

下面從源碼來分析下JVM是如何構(gòu)建內(nèi)置classloader的,具體是rt.jar包里面sun.misc.Launcher類:

public Launcher()  
      {  
        ExtClassLoader localExtClassLoader;  
        try  
        {  //首先創(chuàng)建了ExtClassLoader
          localExtClassLoader = ExtClassLoader.getExtClassLoader();  
        }  
        catch (IOException localIOException1)  
        {  
          throw new InternalError("Could not create extension class loader");  
        }  
        try  
        {  //然后以ExtClassloader作為父加載器創(chuàng)建了AppClassLoader
          this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);  
        }  
        catch (IOException localIOException2)  
        {  
          throw new InternalError("Could not create application class loader");  
        }  //這個(gè)是個(gè)特殊的加載器后面會講到,這里只需要知道默認(rèn)下線程上下文加載器為appclassloader
        Thread.currentThread().setContextClassLoader(this.loader);  
          
        ................
      }  

下面看下ExtClassLoader.getExtClassLoader()的代碼

public static ExtClassLoader getExtClassLoader()  
      throws IOException  
    {  //可以知道ExtClassLoader類加載路徑為java.ext.dirs
      File[] arrayOfFile = getExtDirs();  
      try  
      {  
        (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction()  
        {  
          public Launcher.ExtClassLoader run()  
            throws IOException  
          {  
            int i = this.val$dirs.length;  
            for (int j = 0; j < i; j++) {  
              MetaIndex.registerDirectory(this.val$dirs[j]);  
            }  
            return new Launcher.ExtClassLoader(this.val$dirs);  
          }  
        });  
      }  
      catch (PrivilegedActionException localPrivilegedActionException)  
      {  
        throw ((IOException)localPrivilegedActionException.getException());  
      }  
    }  
      
  
    private static File[] getExtDirs()  
    {  
      String str = System.getProperty("java.ext.dirs");  
      File[] arrayOfFile;  
      if (str != null)  
      {  
        StringTokenizer localStringTokenizer = new StringTokenizer(str, File.pathSeparator);  
          
        int i = localStringTokenizer.countTokens();  
        arrayOfFile = new File[i];  
        for (int j = 0; j < i; j++) {  
          arrayOfFile[j] = new File(localStringTokenizer.nextToken());  
        }  
      }  
      else  
      {  
        arrayOfFile = new File[0];  
      }  
      return arrayOfFile;  
    }  

下面看下AppClassLoader.getAppClassLoader的代碼

public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader)  
      throws IOException  
    {  //可知AppClassLoader類加載路徑為java.class.path
      String str = System.getProperty("java.class.path");  
      final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str);  
        
      (ClassLoader)AccessController.doPrivileged(new PrivilegedAction()  
      {  
        public Launcher.AppClassLoader run()  
        {  
          URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile);  
            
          return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader);  
        }  
      });  
    }  

總結(jié)下Java應(yīng)用啟動過程是首先BootstarpClassloader加載rt.jar包里面的sun.misc.Launcher類,而該類內(nèi)部使用BootstarpClassloader加載器構(gòu)建和初始化Java中三種類加載和線程上下文類加載器,然后在根據(jù)不同場景去使用這些類加載器去自己的類查找路徑去加載類。

三、一種特殊的類加載器-ContextClassLoader

閱讀過tomcat和集團(tuán)中間件源碼的童鞋對ContextClassLoader應(yīng)該很熟悉了,可以很多地方看到下面的結(jié)構(gòu)的用法:

//獲取當(dāng)前線程上下文類加載器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {//設(shè)置當(dāng)前線程上下文類加載器為targetTccl
    Thread.currentThread().setContextClassLoader(targetTccl);
    //doSomething 
    doSomething();
} finally {//設(shè)置當(dāng)前線程上下文加載器為原始加載器
    Thread.currentThread().setContextClassLoader(classLoader);
}

doSomething里面則調(diào)用了 Thread.currentThread().getContextClassLoader()獲取了當(dāng)前線程上下文類加載器來做了某些事情。那么這其中的奧秘和使用場景是哪里那?我們知道Java默認(rèn)的類加載機(jī)制是委托機(jī)制,但是有時(shí)這種加載順序不能正常工作,通常發(fā)生在有些JVM核心代碼必須動態(tài)加載由應(yīng)用程序開發(fā)人員提供的資源時(shí)。以JNDI舉例:它的核心內(nèi)容和接口在rt.jar中的引導(dǎo)類中實(shí)現(xiàn)了,但是這些JNDI實(shí)現(xiàn)類可能加載由獨(dú)立廠商實(shí)現(xiàn)和部署在應(yīng)用程序的classpath中的JNDI提供者。這個(gè)場景要求一個(gè)父類加載器(這個(gè)例子中指加載rt.jar的bootstarp加載器)去加載一個(gè)在它的子類加載器(AppClassLoader)中可見的類。此時(shí)通常的J2SE委托機(jī)制就不能勝任,解決辦法是讓JNDI核心類使用線程上下文加載器(從2.6節(jié)知道默認(rèn)線程上下文加載器為AppClassLoader)。

具體來說在比如Java中的spi,SPI的全名為Service Provider Interface,spi是面向接口編程,服務(wù)規(guī)則提供者會在Java核心類理他提供服務(wù)訪問接口,而具體實(shí)現(xiàn)則有其他開發(fā)商提供,我們知道Java核心類,例如rt.jar包是有bootstrap加載,而用戶提供的jar包在由appclassloader加載。另外我們知道如果一個(gè)類是有A加載器加載,那么A類依賴或者引用的類也是有相同的加載器加載。那么有bootstarp加載的類怎么加載到本來應(yīng)該有appclassloadr加載的類那,這時(shí)候線程上下文類加載就派上用處了。

在 例如jdbc4也基于spi的機(jī)制來發(fā)現(xiàn)驅(qū)動提供商了,可以通過META-INF/services/java.sql.Driver文件里指定實(shí)現(xiàn)類的方式來暴露驅(qū)動提供者.JDBC服務(wù)提供商只需要實(shí)現(xiàn)java.sql.dirver就可以了。例如mysql驅(qū)動


screenshot.png

而mysql的驅(qū)動如下實(shí)現(xiàn)了java.sql.Driver

public class com.mysql.jdbc.Driver extends com.mysql.jdbc.NonRegisteringDriver implements java.sql.Driver

我們寫個(gè)測試代碼,看看是如何工作的:

public class MyContextClassLoad {

     public static void testContextClassLoader() {
            ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
            Iterator<Driver> iterator = loader.iterator();
            while (iterator.hasNext()) {
                Driver driver = (Driver) iterator.next();
                System.out.println("driver:" + driver.getClass() + ",loader:" + driver.getClass().getClassLoader());
            }
        System.out.println("current thread contextloader:"+Thread.currentThread().getContextClassLoader());

            System.out.println("current loader:" + MyContextClassLoad.class.getClassLoader());
        System.out.println("ServiceLoader loader:" + ServiceLoader.class.getClassLoader());
        }
    
    
    public static void main(String []arg){
        
        testContextClassLoader();
    }
}

執(zhí)行結(jié)果如下:
driver:class com.mysql.jdbc.Driver,loader:sun.misc.Launcher$AppClassLoader@53d9f80

current thread context loader:sun.misc.Launcher$AppClassLoader@53d9f80

current loader:sun.misc.Launcher$AppClassLoader@53d9f80
ServiceLoader loader:null
從執(zhí)行結(jié)果可以知道ServiceLoader的加載器為Bootstarp,因?yàn)檫@里輸出了null,并且從該類在rt.jar里面也證明了這個(gè)說法.而com.mysql.jdbc.Driver則使用AppClassLoader加載。我們知道如果一個(gè)類中引用了另外一個(gè)類,那么這被引用的類也應(yīng)該由引用方類加載器來加載,而現(xiàn)在則是引用方ServiceLoader使用BootStarpClassloader加載,被引用方則使用偽子加載器APPClassLoader來加載了,是不是很詭異

下面我們來看下源碼:

public final class ServiceLoader<S> implements Iterable<S> {
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 獲取當(dāng)前線程上下文,本例子里面是AppClassLoader
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
        return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = svc;
        loader = cl;
        reload();
    }

    public S next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            // 這里使用AppClassLoader加載mysql實(shí)現(xiàn)的spi類
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service, "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service, "Provider " + cn + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service, "Provider " + cn + " could not be instantiated", x);
        }
        throw new Error(); // This cannot happen
    }
}

我們在做另外一個(gè)實(shí)驗(yàn):

package myclassloader;

import java.io.FileNotFoundException;
import java.net.URL;
import org.springframework.transaction.annotation.Transactional;

public static void testContextClassLoader() {
        //獲取extclassloader
    ClassLoader extClassloader = MyContextClassLoad.class.getClassLoader().getParent();
   System.out.println("extloader:"  +extClassloader);
       //設(shè)置當(dāng)前線程上下文加載器為ext
     Thread.currentThread().setContextClassLoader(extClassloader);
       ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
       Iterator<Driver> iterator = loader.iterator();
       while (iterator.hasNext()) {
           Driver driver = (Driver) iterator.next();
           System.out.println("driver:" + driver.getClass() + ",loader:" + driver.getClass().getClassLoader());
       }
       System.out.println("current thread context loader:"  +Thread.currentThread().getContextClassLoader());
       System.out.println("current loader:" + MyContextClassLoad.class.getClassLoader());
       System.out.println("ServiceLoader loader:" + ServiceLoader.class.getClassLoader());
   }

extloader:sun.misc.Launcher$ExtClassLoader@10b28f30

current thread context loader:sun.misc.Launcher$ExtClassLoader@10b28f30

current loader:sun.misc.Launcher$AppClassLoader@53d9f80

ServiceLoader loader:null

從結(jié)果可以知道沒有加載到mysql驅(qū)動類,這是因?yàn)閙ysql的jar包是放到了classpath下,而extclassloader查找路徑為ext目錄所致。

總結(jié)下:當(dāng)父類加載器需要加載子類加載器中的資源時(shí)候可以通過設(shè)置和獲取線程上下文類加載器來實(shí)現(xiàn),其實(shí)另外還有一種情況就是一個(gè)類加載器要使用不在當(dāng)前類加載器類查找路徑路徑中的情況,這種情況下可以新建一個(gè)在指定路徑查找類的類加載器,其實(shí)下面第五節(jié)要講的pandora類加載機(jī)制就是這樣。

四、Tomcat ClassLoader

4.1 Tomcat classloader的構(gòu)造

首先我們打開tomcat的源碼Bootstrap類的initClassLoaders方法:

    private void initClassLoaders() {
        try {
            // 創(chuàng)建commonLoader,父類為APPClassLoader
            commonLoader = createClassLoader("common", this.getClass().getClassLoader());

            if( commonLoader == null ) {
                commonLoader=this.getClass().getClassLoader();
            }
           //以commonLoader為父加載器創(chuàng)建catalinaLoader和sharedLoader加載器
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

現(xiàn)在我們已經(jīng)知道這三個(gè)加載器關(guān)系了,下面看下創(chuàng)建加載器的代碼:

private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {
        //獲取catalina.properties中配置,分別為:common.loader,server.loader,shared.loader
        String value = CatalinaProperties.getProperty(name + ".loader");
       
       //根據(jù)配置知道server.loader,shared.loader為空,所以commonLoader=serverLoader=sharedLoader
        if ((value == null) || (value.equals("")))
            return parent;

        value = replace(value);

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

        StringTokenizer tokenizer = new StringTokenizer(value, ",");
        while (tokenizer.hasMoreElements()) {
            String repository = tokenizer.nextToken().trim();
            if (repository.length() == 0) {
                continue;
            }

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

            // Local repository
            if (repository.endsWith("*.jar")) {
                repository = repository.substring
                    (0, repository.length() - "*.jar".length());
                repositories.add(
                        new Repository(repository, RepositoryType.GLOB));
            } else if (repository.endsWith(".jar")) {
                repositories.add(
                        new Repository(repository, RepositoryType.JAR));
            } else {
                repositories.add(
                        new Repository(repository, RepositoryType.DIR));
            }
        }
       //這里其實(shí)是URLClassLoader,只是urls會不同
        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

這里根據(jù)配置文件catalina.properties里面配置的倉庫地址作為類加載器查找類路徑,配置如下:
common.loader=${catalina.base}/lib,${catalina.base}/lib/.jar,${catalina.home}/lib,${catalina.home}/lib/.jar
server.loader=
shared.loader=
根據(jù)上面代碼可知commonLoader,serverLoader,sharedLoader是同一個(gè)classloader。

然后繼續(xù)看bootstarp的init方法:

    public void init()
        throws Exception
    {
        .....
        //上面分析的
        initClassLoaders();

        //設(shè)置當(dāng)前線程上下文加載器為catalinaLoader
        Thread.currentThread().setContextClassLoader(catalinaLoader);
        SecurityClassLoad.securityClassLoad(catalinaLoader);
        
        //使用catalinaLoader加載Catalina類
        Class<?> startupClass =
            catalinaLoader.loadClass
            ("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.newInstance();

        // 設(shè)置Catalina的父加載器為sharedLoader.
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;
    }

這里設(shè)置了Catalina里面的parentClassLoader=sharedLoader,下面我們看web應(yīng)用的classloader如何被創(chuàng)建。

研究過tomcat的童鞋應(yīng)該都知道tomcat的容器構(gòu)造:


screenshot.png

其中Engine是最大的容器默認(rèn)為StandardEngine,其中可以有若干個(gè)host默認(rèn)為StandardHost,host的父容器為Engine,每個(gè)host容器里面有若干context容器默認(rèn)為StandardContext,context容器的父容器為host.

從catalina.java的createStartDigester函數(shù):

  digester.addRule("Server/Service/Engine",
                         new SetParentClassLoaderRule(parentClassLoader));

知道StandardEngine中的parentClassLoader被設(shè)置為了sharedLoader。

然后我們看下StandardContext中的startInternal方法:

 if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
           //設(shè)置到類對象
            setLoader(webappLoader);
        }

其中g(shù)etParentClassLoader方法用來獲取父類加載器這里為sharedLoader,具體原因看如下代碼:

    public ClassLoader getParentClassLoader() {
        if (parentClassLoader != null)
            return (parentClassLoader);
        if (getPrivileged()) {
            return this.getClass().getClassLoader();
        } else if (parent != null) {
            //這里StandardContext的parentClassLoader為空,則會調(diào)用StandardHost的parentClassLoader方法,也為空,則會調(diào)用StandardEngine的parentClassLoader方法,而它返回的正是sharedLoader。
            return (parent.getParentClassLoader());
        }
        return (ClassLoader.getSystemClassLoader());
    }

上面創(chuàng)建了WebappLoader并且設(shè)置到了StandardContext的loader屬性,下面調(diào)用loader的start方法啟動web加載器的創(chuàng)建:

if ((loader != null) && (loader instanceof Lifecycle))
                    ((Lifecycle) loader).start();

進(jìn)入WebappLoader的startInternal方法:

 protected void startInternal() throws LifecycleException {
        ..........
        // Construct a class loader based on our current repositories list
        try {
            if (classLoader == null) {//這里創(chuàng)建web應(yīng)用類加載器
                classLoader = createClassLoader();
            }
            classLoader.setResources(container.getResources());
            classLoader.setDelegate(this.delegate);
            classLoader.setSearchExternalFirst(searchExternalFirst);
            if (container instanceof StandardContext) {
                classLoader.setAntiJARLocking(
                        ((StandardContext) container).getAntiJARLocking());
                classLoader.setClearReferencesRmiTargets(
                        ((StandardContext) container).getClearReferencesRmiTargets());
                classLoader.setClearReferencesStatic(
                        ((StandardContext) container).getClearReferencesStatic());
                classLoader.setClearReferencesStopThreads(
                        ((StandardContext) container).getClearReferencesStopThreads());
                classLoader.setClearReferencesStopTimerThreads(
                        ((StandardContext) container).getClearReferencesStopTimerThreads());
                classLoader.setClearReferencesHttpClientKeepAliveThread(
                        ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
            }

            for (int i = 0; i < repositories.length; i++) {
                classLoader.addRepository(repositories[i]);
            }

        } catch (Throwable t) {
        .......
        }

        setState(LifecycleState.STARTING);
    }

createClassLoader的代碼如下:

 /**
     * Create associated classLoader.
     */
private String loaderClass =
        "org.apache.catalina.loader.WebappClassLoader";
    private WebappClassLoaderBase createClassLoader()
        throws Exception {
       //創(chuàng)建WebappClassLoader類
        Class<?> clazz = Class.forName(loaderClass);
        WebappClassLoaderBase classLoader = null;

        if (parentClassLoader == null) {
            parentClassLoader = container.getParentClassLoader();
        }
       //設(shè)置WebappClassLoader的父加載器為sharedLoader
        Class<?>[] argTypes = { ClassLoader.class };
        Object[] args = { parentClassLoader };
        Constructor<?> constr = clazz.getConstructor(argTypes);
        classLoader = (WebappClassLoaderBase) constr.newInstance(args);

        return classLoader;

    }

至此創(chuàng)建了應(yīng)用的類加載器,由于每個(gè)standardcontext對應(yīng)一個(gè)web應(yīng)用,所以不同的應(yīng)用都有不同的
WebappClassLoader,共同點(diǎn)是他們的父加載器都是sharedLoader。下面列下tomcat類加載器關(guān)系圖:


screenshot.png

4.2 Tomcat classloader

tomcat原生加載器:


screenshot.png

原生tomcat加載器里面查找,下面說說tomcat自身的web類加載器邏輯:

 public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLockInternal(name)) {
            ........
            Class<?> clazz = null;
    
            ........
            // (0) 首先檢查webloader緩存中是否已經(jīng)加載該類
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
    
            // (0.1) 看jvm緩存是否已經(jīng)加載該類
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
    
            // (0.2) 嘗試在extClassloader查找該類
            try {
                clazz = j2seClassLoader.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
    
           .....
    
            boolean delegateLoad = delegate || filter(name);
    
            // (1) 如果在context.xml配置了代理,則委托給父類加載器sharedclassloader來加載
            if (delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader1 " + parent);
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
    
            // (2) 在web應(yīng)用的WEB-INF/lib下查找
        
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from local repository");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
    
            // (3) 如果沒有context.xml配置代理,則委托給父類加載器sharedclassloader來加載,和(1)只有一個(gè)存在
            if (!delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader at end: " + parent);
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }
        
        throw new ClassNotFoundException(name);
    }

其中(0.2) 之前一直以為是appclassloader來加載,但是源碼后發(fā)現(xiàn)竟然是extclassloader.

public WebappClassLoaderBase(ClassLoader parent) {

        super(new URL[0], parent);

        ClassLoader p = getParent();
        if (p == null) {
            p = getSystemClassLoader();
        }
        this.parent = p;
        
        ClassLoader j = String.class.getClassLoader();
        if (j == null) {
            j = getSystemClassLoader();
            while (j.getParent() != null) {
                j = j.getParent();
            }
        }
        this.j2seClassLoader = j;

        securityManager = System.getSecurityManager();
        if (securityManager != null) {
            refreshPolicy();
        }
    }

其實(shí)在tomcat里面使用ContextClassloader的地方也隨處可以,還比如StandardContext中的startInternal方法,我們知道StandardContext是有catalinaclassloader加載的,而startInternal里面則可以創(chuàng)建我們在web.xml中配置的listener和filter(這些明顯應(yīng)該由webappclassloader加載)

//綁定當(dāng)前線程上下文加載器為webappclassloader
oldCCL = bindThread();

try {
   
    // Configure and call application event listeners
    if (ok) {
        if (!listenerStart()) {
         
            ok = false;
        }
    }
    
   
    // Configure and call application filters
    if (ok) {
        if (!filterStart()) {
            log.error(sm.getString("standardContext.filterFail"));
            ok = false;
        }
    }
    
    // Load and initialize all "load on startup" servlets
    if (ok) {
        if (!loadOnStartup(findChildren())){
            log.error(sm.getString("standardContext.servletFail"));
            ok = false;
        }
    }
    
    // Start ContainerBackgroundProcessor thread
    super.threadStart();
} finally {
    // Unbinding thread
    unbindThread(oldCCL);
}

總結(jié)下,默認(rèn)情況下tomcat中commonloader,sharedloader,catalinaloader是同一個(gè)加載器,其類查找路徑都是同一個(gè)地方。其實(shí)catalinaloader主要工作應(yīng)該是加載tomcat本身啟動所需要的類,而sharedloader是webappclassloader的父類,所以應(yīng)該是加載一些所有webap共享的類,而commonlaoder作為sharedloader,catalinaloader的父類,自然設(shè)計(jì)目的是為了加載二者共享的類。所以如果能恰當(dāng)?shù)氖褂胻omcat中設(shè)計(jì)的這種策略,修改catalina.properites中三種加載器類加載路徑,就會真正達(dá)到這種設(shè)計(jì)效果。

歡迎關(guān)注微信公眾號:‘技術(shù)原始積累’ 獲取更多技術(shù)干貨__

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容