ClassLoader解惑

一锦针、什么是Classloader

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

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

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

二毡熏、Java自帶的Classloader

2.1 BootstrapClassloader

引導(dǎo)類加載器坦敌,又稱啟動(dòng)類加載器,是最頂層的類加載器痢法,主要用來(lái)加載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 語(yǔ)言實(shí)現(xiàn),Java程序訪問(wèn)不到該加載器提茁。通過(guò)下面代碼可以查看該加載器加載了哪些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)該都知道,我們并沒(méi)有在classpath里面指定這些類的路徑茴扁,為啥還是能被加載到j(luò)vm并使用起來(lái)了吧铃岔,因?yàn)檫@些是bootstarp來(lái)加載的。

2.2 ExtClassloader

擴(kuò)展類加載器峭火,主要負(fù)責(zé)加載Java的擴(kuò)展類庫(kù)毁习,默認(rèn)加載JAVA_HOME/jre/lib/ext/目下的所有jar包或者由java.ext.dirs系統(tǒng)屬性指定的jar包。放入這個(gè)目錄下的jar包對(duì)所有AppClassloader都是可見(jiàn)的(后面會(huì)知道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)用加載器,本文說(shuō)的SystemClassloader和APPClassloader是一個(gè)東西坯苹,它負(fù)責(zé)在JVM啟動(dòng)時(shí)隆檀,加載來(lái)自在命令java中的-classpath或者java.class.path系統(tǒng)屬性或者 CLASSPATH操作系統(tǒng)屬性所指定的JAR類包和類路徑。調(diào)用ClassLoader.getSystemClassLoader()可以獲取該類加載器粹湃。如果沒(méi)有特別指定乾吻,則用戶自定義的任何類加載器都將該類加載器作為它的父加載器,這點(diǎn)通過(guò)ClassLoader的無(wú)參構(gòu)造函數(shù)可以知道如下:

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

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

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

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

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


screenshot.png

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

ClassLoader.getSystemClassLoader().getParent()

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

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

2.5 類加載器原理

Java類加載器使用的是委托機(jī)制择葡,也就是子類加載器在加載一個(gè)類時(shí)候會(huì)讓父類來(lái)加載,那么問(wèn)題來(lái)了纯丸,為啥使用這種方式那?因?yàn)檫@樣可以避免重復(fù)加載,當(dāng)父親已經(jīng)加載了該類的時(shí)候静袖,就沒(méi)有必要子ClassLoader再加載一次觉鼻。考慮到安全因素队橙,我們?cè)囅胍幌伦钩拢绻皇褂眠@種委托模式,那我們就可以隨時(shí)使用自定義的String來(lái)動(dòng)態(tài)替代java核心api中定義的類型捐康,這樣會(huì)存在非常大的安全隱患仇矾,而雙親委托的方式,就可以避免這種情況解总,因?yàn)镾tring已經(jīng)在啟動(dòng)時(shí)就被引導(dǎo)類加載器(Bootstrcp ClassLoader)加載贮匕,所以用戶自定義的ClassLoader永遠(yuǎn)也無(wú)法加載一個(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) {  
                   // 若仍然沒(méi)有找到則調(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)證乌昔,符號(hào)引用轉(zhuǎn)為直接引用等
           }  
           returnc;  
       }  
   }  

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

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

下面從源碼來(lái)分析下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è)特殊的加載器后面會(huì)講到漓雅,這里只需要知道默認(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)用啟動(dòng)過(guò)程是首先BootstarpClassloader加載rt.jar包里面的sun.misc.Launcher類录别,而該類內(nèi)部使用BootstarpClassloader加載器構(gòu)建和初始化Java中三種類加載和線程上下文類加載器,然后在根據(jù)不同場(chǎng)景去使用這些類加載器去自己的類查找路徑去加載類邻吞。

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

閱讀過(guò)tomcat和集團(tuán)中間件源碼的童鞋對(duì)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)前線程上下文類加載器來(lái)做了某些事情抱冷。那么這其中的奧秘和使用場(chǎng)景是哪里那崔列?我們知道Java默認(rèn)的類加載機(jī)制是委托機(jī)制,但是有時(shí)這種加載順序不能正常工作旺遮,通常發(fā)生在有些JVM核心代碼必須動(dòng)態(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è)場(chǎng)景要求一個(gè)父類加載器(這個(gè)例子中指加載rt.jar的bootstarp加載器)去加載一個(gè)在它的子類加載器(AppClassLoader)中可見(jiàn)的類边翼。此時(shí)通常的J2SE委托機(jī)制就不能勝任猪贪,解決辦法是讓JNDI核心類使用線程上下文加載器(從2.6節(jié)知道默認(rèn)線程上下文加載器為AppClassLoader)。

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

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


screenshot.png

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

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

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

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è)說(shuō)法.而com.mysql.jdbc.Driver則使用AppClassLoader加載茄螃。我們知道如果一個(gè)類中引用了另外一個(gè)類缝驳,那么這被引用的類也應(yīng)該由引用方類加載器來(lái)加載,而現(xiàn)在則是引用方ServiceLoader使用BootStarpClassloader加載归苍,被引用方則使用偽子加載器APPClassLoader來(lái)加載了用狱,是不是很詭異

下面我們來(lái)看下源碼:

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
    }
}

我們?cè)谧隽硗庖粋€(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é)果可以知道沒(méi)有加載到mysql驅(qū)動(dòng)類拼弃,這是因?yàn)閙ysql的jar包是放到了classpath下夏伊,而extclassloader查找路徑為ext目錄所致。

總結(jié)下:當(dāng)父類加載器需要加載子類加載器中的資源時(shí)候可以通過(guò)設(shè)置和獲取線程上下文類加載器來(lái)實(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會(huì)不同
        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

這里根據(jù)配置文件catalina.properties里面配置的倉(cāng)庫(kù)地址作為類加載器查找類路徑榜晦,配置如下:
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)建。

研究過(guò)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è)置到類對(duì)象
            setLoader(webappLoader);
        }

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

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

上面創(chuàng)建了WebappLoader并且設(shè)置到了StandardContext的loader屬性,下面調(diào)用loader的start方法啟動(dòng)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對(duì)應(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加載器里面查找乃秀,下面說(shuō)說(shuō)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來(lái)加載
            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) 如果沒(méi)有context.xml配置代理圆兵,則委托給父類加載器sharedclassloader來(lái)加載跺讯,和(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來(lái)加載,但是源碼后發(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)建我們?cè)趙eb.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本身啟動(dòng)所需要的類轮傍,而sharedloader是webappclassloader的父類暂雹,所以應(yīng)該是加載一些所有webap共享的類,而commonlaoder作為sharedloader,catalinaloader的父類创夜,自然設(shè)計(jì)目的是為了加載二者共享的類杭跪。所以如果能恰當(dāng)?shù)氖褂胻omcat中設(shè)計(jì)的這種策略,修改catalina.properites中三種加載器類加載路徑,就會(huì)真正達(dá)到這種設(shè)計(jì)效果涧尿。

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

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末系奉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子姑廉,更是在濱河造成了極大的恐慌缺亮,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桥言,死亡現(xiàn)場(chǎng)離奇詭異萌踱,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)号阿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門并鸵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人倦西,你說(shuō)我怎么就攤上這事×扪希” “怎么了扰柠?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)疼约。 經(jīng)常有香客問(wèn)我卤档,道長(zhǎng),這世上最難降的妖魔是什么程剥? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任劝枣,我火速辦了婚禮,結(jié)果婚禮上织鲸,老公的妹妹穿的比我還像新娘舔腾。我一直安慰自己,他們只是感情好搂擦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布稳诚。 她就那樣靜靜地躺著,像睡著了一般瀑踢。 火紅的嫁衣襯著肌膚如雪扳还。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天橱夭,我揣著相機(jī)與錄音氨距,去河邊找鬼。 笑死棘劣,一個(gè)胖子當(dāng)著我的面吹牛俏让,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼舆驶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼橱健!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起沙廉,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拘荡,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后撬陵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體珊皿,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年巨税,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蟋定。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡草添,死狀恐怖驶兜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情远寸,我是刑警寧澤抄淑,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站驰后,受9級(jí)特大地震影響肆资,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灶芝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一郑原、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧夜涕,春花似錦犯犁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至晓避,卻和暖如春簇捍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背俏拱。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工暑塑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锅必。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓事格,卻偏偏與公主長(zhǎng)得像惕艳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子驹愚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容