圖解Tomcat類加載機制(阿里面試題)

Tomcat的類加載機制是違反了雙親委託原則的,對於一些未加載的非基礎類(Object,String等),各個web應用本身的類加載器(WebAppClassLoader)會優先加載,加載不到時再交給commonClassLoader走雙親委託。 javascript

對於JVM來講:php

所以,按照這個過程能夠想到,若是一樣在CLASSPATH指定的目錄中和本身工做目錄中存放相同的class,會優先加載CLASSPATH目錄中的文件。html

 

 

 一、既然 Tomcat 不遵循雙親委派機制,那麼若是我本身定義一個惡意的HashMap,會不會有風險呢?(阿里的面試官問)java

答: 顯然不會有風險,若是有,Tomcat都運行這麼多年了,那羣Tomcat大神能不改進嗎? tomcat不遵循雙親委派機制,只是自定義的classLoader順序不一樣,但頂層仍是相同的,web

仍是要去頂層請求classloader.面試

二、咱們思考一下:Tomcat是個web容器, 那麼它要解決什麼問題: 
1. 一個web容器可能須要部署兩個應用程序,不一樣的應用程序可能會依賴同一個第三方類庫的不一樣版本,不能要求同一個類庫在同一個服務器只有一份,所以要保證每一個應用程序的類庫都是獨立的,保證相互隔離。 
2. 部署在同一個web容器中相同的類庫相同的版本能夠共享。不然,若是服務器有10個應用程序,那麼要有10份相同的類庫加載進虛擬機,這是扯淡的。 
3. web容器也有本身依賴的類庫,不能於應用程序的類庫混淆。基於安全考慮,應該讓容器的類庫和程序的類庫隔離開來。 
4. web容器要支持jsp的修改,咱們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行後修改jsp已是司空見慣的事情,不然要你何用? 因此,web容器須要支持 jsp 修改後不用重啓。bootstrap

再看看咱們的問題:Tomcat 若是使用默認的類加載機制行不行? 
答案是不行的。爲何?咱們看,第一個問題,若是使用默認的類加載器機制,那麼是沒法加載兩個相同類庫的不一樣版本的,默認的累加器是無論你是什麼版本的,只在意你的全限定類名,而且只有一份。第二個問題,默認的類加載器是可以實現的,由於他的職責就是保證惟一性。第三個問題和第一個問題同樣。咱們再看第四個問題,咱們想咱們要怎麼實現jsp文件的熱修改(樓主起的名字),jsp 文件其實也就是class文件,那麼若是修改了,但類名仍是同樣,類加載器會直接取方法區中已經存在的,修改後的jsp是不會從新加載的。那麼怎麼辦呢?咱們能夠直接卸載掉這jsp文件的類加載器,因此你應該想到了,每一個jsp文件對應一個惟一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。從新建立類加載器,從新加載jsp文件。api

Tomcat 如何實現本身獨特的類加載機制?

因此,Tomcat 是怎麼實現的呢?牛逼的Tomcat團隊已經設計好了。咱們看看他們的設計圖:緩存

咱們看到,前面3個類加載和默認的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat本身定義的類加載器,它們分別加載/common/*/server/*/shared/*(在tomcat 6以後已經合併到根目錄下的lib目錄下)和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和Jsp類加載器一般會存在多個實例,每個Web應用程序對應一個WebApp類加載器,每個JSP文件對應一個Jsp類加載器。tomcat

  • commonLoader:Tomcat最基本的類加載器,加載路徑中的class能夠被Tomcat容器自己以及各個Webapp訪問;
  • catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對於Webapp不可見;
  • sharedLoader:各個Webapp共享的類加載器,加載路徑中的class對於全部Webapp可見,可是對於Tomcat容器不可見;
  • WebappClassLoader:各個Webapp私有的類加載器,加載路徑中的class只對當前Webapp可見;

從圖中的委派關係中能夠看出:

CommonClassLoader能加載的類均可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader本身能加載的類則與對方相互隔離。

WebAppClassLoader可使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。

而JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並經過再創建一個新的Jsp類加載器來實現JSP文件的HotSwap功能。

好了,至此,咱們已經知道了tomcat爲何要這麼設計,以及是如何設計的,那麼,tomcat 違背了java 推薦的雙親委派模型了嗎?答案是:違背了。 咱們前面說過:

雙親委派模型要求除了頂層的啓動類加載器以外,其他的類加載器都應當由本身的父類加載器加載。

很顯然,tomcat 不是這樣實現,tomcat 爲了實現隔離性,沒有遵照這個約定,每一個webappClassLoader加載本身的目錄下的class文件,不會傳遞給父類加載器。

咱們擴展出一個問題:若是tomcat 的 Common ClassLoader 想加載 WebApp ClassLoader 中的類,該怎麼辦?

看了前面的關於破壞雙親委派模型的內容,咱們內心有數了,咱們可使用線程上下文類加載器實現,使用線程上下文加載器,可讓父類加載器請求子類加載器去完成類加載的動做。牛逼吧。

 

 

類加載

  在JVM中並非一次性把全部的文件都加載到,而是一步一步的,按照須要來加載。

  好比JVM啓動時,會經過不一樣的類加載器加載不一樣的類。當用戶在本身的代碼中,須要某些額外的類時,再經過加載機制加載到JVM中,而且存放一段時間,便於頻繁使用。

  所以使用哪一種類加載器、在什麼位置加載類都是JVM中重要的知識。

JVM類加載

  JVM類加載採用 父類委託機制,以下圖所示:

  JVM中包括集中類加載器:

  1 BootStrapClassLoader 引導類加載器

  2 ExtClassLoader 擴展類加載器

  3 AppClassLoader 應用類加載器

  4 CustomClassLoader 用戶自定義類加載器

  他們的區別上面也都有說明。須要注意的是,不一樣的類加載器加載的類是不一樣的,所以若是用戶加載器1加載的某個類,其餘用戶並不可以使用。

 

  當JVM運行過程當中,用戶須要加載某些類時,會按照下面的步驟(父類委託機制)

  1 用戶本身的類加載器,把加載請求傳給父加載器,父加載器再傳給其父加載器,一直到加載器樹的頂層。

  2 最頂層的類加載器首先針對其特定的位置加載,若是加載不到就轉交給子類。

  3 若是一直到底層的類加載都沒有加載到,那麼就會拋出異常ClassNotFoundException。

  所以,按照這個過程能夠想到,若是一樣在CLASSPATH指定的目錄中和本身工做目錄中存放相同的class,會優先加載CLASSPATH目錄中的文件。

Tomcat類加載

  在tomcat中類的加載稍有不一樣,以下圖:

  當tomcat啓動時,會建立幾種類加載器:

  1 Bootstrap 引導類加載器 

  加載JVM啓動所需的類,以及標準擴展類(位於jre/lib/ext下)

  2 System 系統類加載器 

  加載tomcat啓動的類,好比bootstrap.jar,一般在catalina.bat或者catalina.sh中指定。位於CATALINA_HOME/bin下。

  3 Common 通用類加載器 

  加載tomcat使用以及應用通用的一些類,位於CATALINA_HOME/lib下,好比servlet-api.jar

  4 webapp 應用類加載器

  每一個應用在部署後,都會建立一個惟一的類加載器。該類加載器會加載位於 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。

 

  當應用須要到某個類時,則會按照下面的順序進行類加載

  1 使用bootstrap引導類加載器加載

  2 使用system系統類加載器加載

  3 使用應用類加載器在WEB-INF/classes中加載

  4 使用應用類加載器在WEB-INF/lib中加載

  5 使用common類加載器在CATALINA_HOME/lib中加載

 

問題擴展

  經過對上面tomcat類加載機制的理解,就不難明白 爲何java文件放在Eclipse中的src文件夾下會優先jar包中的class?

  這是由於Eclipse中的src文件夾中的文件java以及webContent中的JSP都會在tomcat啓動時,被編譯成class文件放在 WEB-INF/class 中。

  而Eclipse外部引用的jar包,則至關於放在 WEB-INF/lib 中。

  所以確定是 java文件或者JSP文件編譯出的class優先加載

  經過這樣,咱們就能夠簡單的把java文件放置在src文件夾中,經過對該java文件的修改以及調試,便於學習擁有源碼java文件、卻沒有打包成xxx-source的jar包。

 

  另外呢,開發者也會由於粗心而犯下面的錯誤。

  在 CATALINA_HOME/lib 以及 WEB-INF/lib 中放置了 不一樣版本的jar包,此時就會致使某些狀況下報加載不到類的錯誤。

  還有若是多個應用使用同一jar包文件,當放置了多份,就可能致使 多個應用間 出現類加載不到的錯誤。

 

 

1. java類加載器

近來了解tomcat的類加載機制,因此先回顧一下java虛擬機類加載器,若是從java虛擬機的角度來看的話,其實類加載器只分爲兩種:一種是啓動類加載器(即Bootstrap ClassLoader),經過使用JNI來實現,咱們沒法獲取到到它的實例;另外一種則是java語言實現java.lang.ClassLoader的子類。通常從咱們的角度來看,會根據類加載路徑會把類加載器分爲3種:Bootstrap ClassLoader,ExtClassLoader,AppClassLoader.後二者是sun.misc.Launcher類的內部類,而前者在JDK源碼中是沒有與之對應的類的,卻是在sun.misc.Launcher中能夠看到一些它的加載路徑信息。若是找不到sun的源碼,能夠下載OpenJDK的來看一下。

Bootstrap ClassLoader: 引導類加載器,從%JAVA_RUNTIME_JRE%/lib目錄加載,但並非將該目錄全部的類庫都加載,它會加載一些符合文件名稱的,例如:rt.jar,resources.jar等。在sun.misc.Launcher源碼中也能夠看得它的加載路徑:

private static String bootClassPath = System.getProperty("sun.boot.class.path");
  • 1

或者配置-Xbootclasspath參數指定加載的路徑,經過獲取環境變量sun.boot.class.path看一下到底具體加載了那些類:

D:\Program Files\Java\jdk1.7.0_67\jre\lib\resources.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\rt.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\sunrsasign.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jce.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.7.0_67\jre\classes
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Extension ClassLoader:擴展類加載器,實現類爲sun.misc.Launcher$ExtClassLoader,加載%JAVA_RUNTIME_JRE%/lib/ext/目錄下的jar包,也能夠在sun.misc.Launcher源碼中也能夠看得它的加載路徑:

String s = System.getProperty("java.ext.dirs");
  • 1

經過獲取java.ext.dirs環境變量打印一下:

D:\Program Files\Java\jdk1.7.0_67\jre\lib\ext
  • 1

Appication ClassLoader:應用程序類加載器,或者叫系統類加載器,實現類爲sun.misc.Launcher$AppClassLoader。從sun.misc.Launcher的構造函數中能夠看到,當AppClassLoader被初始化之後,它會被設置爲當前線程的上下文類加載器以及保存到Launcher類的loader屬性中,而經過ClassLoader.getSystemClassLoader()獲取的也正是該類加載器(Launcher.loader)。應用類加載器從用戶類路徑中加載類庫,能夠在源碼中看到:

final String s = System.getProperty("java.class.path");
  • 1

1.1 類關係

classloader-1

由圖看到Bootstrap ClassLoader並不在繼承鏈上,由於它是java虛擬機內置的類加載器,對外不可見。能夠看到頂層ClassLoader有一個parent屬性,用來表示着類加載器之間的層次關係(雙親委派模型);注意,ExtClassLoader類在初始化時顯式指定了parent爲null,因此它的父類加載器默認爲Bootstrap ClassLoader。在tomcat中都是經過擴展URLClassLoader來實現本身的類加載器。

1.2 雙親委託模型

這3種類加載器之間存在着父子關係(區別於java裏的繼承),子加載器保存着父加載器的引用。當一個類加載器須要加載一個目標類時,會先委託父加載器去加載,而後父加載器會在本身的加載路徑中搜索目標類,父加載器在本身的加載範圍中找不到時,纔會交還給子加載器加載目標類。

採用雙親委託模式能夠避免類加載混亂,並且還將類分層次了,例如java中lang包下的類在jvm啓動時就被啓動類加載器加載了,而用戶一些代碼類則由應用程序類加載器(AppClassLoader)加載,基於雙親委託模式,就算用戶定義了與lang包中同樣的類,最終仍是由應用程序類加載器委託給啓動類加載器去加載,這個時候啓動類加載器發現已經加載過了lang包下的類了,因此二者都不會再從新加載。固然,若是使用者經過自定義的類加載器能夠強行打破這種雙親委託模型,但也不會成功的,java安全管理器拋出將會拋出java.lang.SecurityException異常。

classloader-2

  1. 啓動類加載器是擴展類加載器的父類加載器:擴展類加載器在sun.misc.Launcher構造函數中被初始化,它的父類加載器被設置了爲null,那爲何還說啓動類加載器是它的父加載器?看一下ClassLoader.loadClass()方法:
 
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,查找該類是否已經被加載過了
            Class c = findLoadedClass(name);
            if (c == null) {  //未被加載過
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {  // 父類加載器不爲null,則調用父類加載器嘗試加載
                        c = parent.loadClass(name, false);
                    } else {   // 父類加載器爲null,則調用本地方法,交由啓動類加載器加載,因此說ExtClassLoader的父類加載器爲Bootstrap ClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                if (c == null) { //仍然加載不到,只能由本加載器經過findClass去加載
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // 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);
            }
            return c;
        }
    }
 

 

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

從代碼中看到,若是parent==null,將會由啓動類加載器嘗試加載,因此擴展類加載器的父類加載器是啓動類加載器。

  1. 擴展類加載器是應用程序類加載器的父類加載器:這個比較好理解,依然是在sun.misc.Launcher構造函數初始化應用程序類加載器時,指定了ExtClassLoader爲AppClassLoader的父類加載器:
loader = AppClassLoader.getAppClassLoader(extcl);//loader是ClassLoader的屬性,extcl是擴展類加載器實例
  • 1
  1. 應用程序類加載器是自定義類加載器的父類加載器:這裏指的是使用默認構造函數進行自定義類加載器(不然 你能夠指定parent來構造一個父加載器爲ExtClassLoader的自定義類加載器),不管是經過擴展ClassLoader仍是URLClassLoader最終都會獲取系統類加載器(AppClassLoader)做爲父類加載器:
 
protected ClassLoader() {
        //調用getSystemClassLoader方法獲取系統類加載器做爲父類加載器
        this(checkCreateClassLoader(), getSystemClassLoader()); 
    }
public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader(); //初始化系統類加載器
        .....
        return scl;
    }
private static synchronized void initSystemClassLoader() {
        ......
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        ......
        scl = l.getClassLoader();  //這裏拿到的就是在Launcher構造函數中構造的AppClassLoader實例
        ......
        }
    }
 

 

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

2. tomcat7類加載器

tomcat做爲一個java web容器,也有本身的類加載機制,經過自定義的類加載機制以實現共享類庫的抽取,不一樣web應用之間的資源隔離還有熱加載等功能。除了一些java自身的一些類加載器處,它實現的主要類加載器有:Common ClassLoader,Catalina ClassLoader,Shared ClassLoader以及WebApp ClassLoader.經過下面類關係圖以及邏輯關係圖,同時對比上文內容梳理這些類加載器之間的關係。

2.1 類關係圖

classloader-3

從圖中看到了Common,Catalina,Shared類加載器是URLClassLoader類的一個實例,只是它們的類加載路徑不同,在tomcat/conf/catalina.properties配置文件中配置(common.loader,server.loader,shared.loader).WebAppClassLoader繼承自WebAppClassLoaderBase,基本全部邏輯都在WebAppClassLoaderBase爲中實現了,能夠看出tomcat的全部類加載器都是以URLClassLoader爲基礎進行擴展。

2.2 邏輯關係圖

classloader-4

上面說到Common,Catalina,Shared類加載器是URLClassLoader類的一個實例,在默認的配置中,它們其實都是同一個對象,即commonLoader,結合初始化時的代碼(只保留關鍵代碼):

 
 private void initClassLoaders() {
        commonLoader = createClassLoader("common", null);  // commonLoader的加載路徑爲common.loader
        if( commonLoader == null ) {
            commonLoader=this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader); // 加載路徑爲server.loader,默認爲空,父類加載器爲commonLoader
        sharedLoader = createClassLoader("shared", commonLoader); // 加載路徑爲shared.loader,默認爲空,父類加載器爲commonLoader
    }
 private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;      // catalinaLoader與sharedLoader的加載路徑均爲空,因此直接返回commonLoader對象,默認3者爲同一個對象
    }
 

 

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在上面的代碼初始化時很明確是指出了,catalina與shared類加載器的父類加載器爲common類加載器,而初始化commonClassLoader時父類加載器設置爲null,最終會調到createClassLoader靜態方法:

 
 public static ClassLoader createClassLoader(List<Repository> repositories,
                                                final ClassLoader parent)
        throws Exception {
        .....
        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            return new URLClassLoader(array);  //該構造方法默認獲取系統類加載器爲父類加載器,即AppClassLoader
                        else
                            return new URLClassLoader(array, parent);
                    }
                });

    }
 

 

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

createClassLoader中指定參數parent==null時,最終會以系統類加載器(AppClassLoader)做爲父類加載器,這解釋了爲何commonClassLoader的父類加載器是AppClassLoader.

一個web應用對應着一個StandardContext實例,每一個web應用都擁有獨立web應用類加載器(WebClassLoader),這個類加載器在StandardContext.startInternal()中被構造了出來:

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

 

 
  • 1
  • 2
  • 3
  • 4
  • 5

這裏getParentClassLoader()會獲取父容器StandarHost.parentClassLoader對象屬性,而這個對象屬性是在Catalina$SetParentClassLoaderRule.begin()初始化,初始化的值其實就是Catalina.parentClassLoader對象屬性,再來跟蹤一下Catalina.parentClassLoader,在Bootstrap.init()時經過反射調用了Catalina.setParentClassLoader(),將Bootstrap.sharedLoader屬性設置爲Catalina.parentClassLoader,因此WebClassLoader的父類加載器是Shared ClassLoader.

2.3 類加載邏輯

tomcat的類加載機制是違反了雙親委託原則的,對於一些未加載的非基礎類(Object,String等),各個web應用本身的類加載器(WebAppClassLoader)會優先加載,加載不到時再交給commonClassLoader走雙親委託。具體的加載邏輯位於WebAppClassLoaderBase.loadClass()方法中,代碼篇幅長,這裏以文字描述加載一個類過程:

  1. 先在本地緩存中查找是否已經加載過該類(對於一些已經加載了的類,會被緩存在resourceEntries這個數據結構中),若是已經加載即返回,不然 繼續下一步。
  2. 讓系統類加載器(AppClassLoader)嘗試加載該類,主要是爲了防止一些基礎類會被web中的類覆蓋,若是加載到即返回,返回繼續。
  3. 前兩步均沒加載到目標類,那麼web應用的類加載器將自行加載,若是加載到則返回,不然繼續下一步。
  4. 最後仍是加載不到的話,則委託父類加載器(Common ClassLoader)去加載。

第3第4兩個步驟的順序已經違反了雙親委託機制,除了tomcat以外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();等不少地方都同樣是違反了雙親委託。

 

 

 

參考:圖解Tomcat類加載機制

參考:深刻理解 Tomcat(四)Tomcat 類加載器之爲什麼違背雙親委派模型

參考java與tomcat7類加載機制 

相關文章
相關標籤/搜索