1. 程式人生 > >java與tomcat7類載入機制

java與tomcat7類載入機制

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");

或者配置-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\jdk
1.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

Extension ClassLoader:擴充套件類載入器,實現類為sun.misc.Launcher$ExtClassLoader,載入%JAVA_RUNTIME_JRE%/lib/ext/目錄下的jar包,也可以在sun.misc.Launcher原始碼中也可以看得它的載入路徑:

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

通過獲取java.ext.dirs環境變數列印一下:

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

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 類關係

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

從程式碼中看到,如果parent==null,將會由啟動類載入器嘗試載入,所以擴充套件類載入器的父類載入器是啟動類載入器。

  1. 擴充套件類載入器是應用程式類載入器的父類載入器:這個比較好理解,依然是在sun.misc.Launcher建構函式初始化應用程式類載入器時,指定了ExtClassLoader為AppClassLoader的父類載入器:
loader = AppClassLoader.getAppClassLoader(extcl);//loader是ClassLoader的屬性,extcl是擴充套件類載入器例項
  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例項
        ......
        }
    }

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者為同一個物件
    }

在上面的程式碼初始化時很明確是指出了,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);
                    }
                });

    }

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

這裡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();等很多地方都一樣是違反了雙親委託。

3. 參考