Tomcat源碼解析-類的加載機制

1 Java 虛擬機中類加載器

在JVM中定義了4類加載器分別為:啟動(Bootstrap)類加載器擴展(Extension)類加載器系統(System)類加載器,以及用戶自定義加載器

image

啟動(Bootstrap)類加載器

引導類加載器是負責加載并管理<JAVA_HOME>/lib 的核心類庫 -Xbootclasspath選項指定的jar包class文件對應的Class對象。

啟動(Bootstrap)類加載器是擴展(Extension)類加載器的父加載器,是最高等級的加載器,由于啟動類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以 不允許直接通過引用進行操作

//從系統屬性中獲取啟動(Bootstrap)類加載器加載并管理核心類庫
System.getProperty("sun.boot.class.path")
C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\classes

擴展(Extension)類加載器

擴展類加載器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的,它負責加載并管理 <JAVA_HOME >/lib/ext或者由系統變量-Djava.ext.dir指定位置中class文件對應的Class對象。

//從系統屬性中獲取擴展(Extension)類加載器加載并管理核心類庫
System.getProperty("java.ext.dirs")
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext;
C:\windows\Sun\Java\lib\ext

系統(System)類加載器

系統類加載器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的,它負責加載并管理用戶類路徑(java -classpath或-Djava.class.path變量所指定的URL資源)中class文件對應的Class對象。開發者可以直接使用系統類加載器。

//從系統屬性中獲取系統(System)類加載器加載并管理核心類庫
System.getProperty("java.class.path")
...省略
D:\project_alibaba\jvm-in-action\target\classes;
...省略

案例

package jvm;
import java.lang.reflect.Constructor;

/**獲取JVM類加載器 **/
public class ClassLoaderTest {
    public static void main(String[] args) {
        try {
            System.out.println(ClassLoader.getSystemClassLoader());
            System.out.println(ClassLoader.getSystemClassLoader().getParent());
            System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@45ee12a7
null
2 類加載(雙親委派機制)

首先檢查這個類是不是已經被加載過了,如果加載過了直接返回,否則交給父加載器去加載。請你注意,這是一個遞歸調用,也就是說子加載器持有父加載器的引用,當一個類加載器需要加載一個 Java 類時,會先委托父加載器去加載,然后父加載器在自己的加載路徑中搜索 Java 類,當父加載器在自己的加載范圍內找不到時,才會交還給子加載器加載,直到最終交給不存在父類加載器的啟動(Bootstrap)類加載器加載。

[圖片上傳失敗...(image-d354d5-1564996480026)]

實現流程

雙親委派模型對于保證Java程序的穩定運作很重要,它的具體實現在java.lang.ClassLoader類loadClass()方法中。

public Class<?> loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}  

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

    // 首先判斷該類是否已經被加載  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        //如果沒有被加載,就委托給父類加載
        try {  
            if (parent != null) {  
                //如果存在父類加載器,就委派給父類加載器加載,這里是一個遞歸調用的過程。  
                c = parent.loadClass(name, false);  
            } else {    // 遞歸終止條件
                // 由于啟動類加載器無法被Java程序直接引用,因此默認用 null 替代
                // parent == null就意味著由啟動類加載器嘗試加載該類,  
                // 即通過調用 native方法 findBootstrapClass0(String name)加載  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果父類加載器不能完成加載請求時,再調用自身的findClass方法進行類加載,若加載成功,findClass方法返回的是defineClass方法的返回值
            // 注意,若自身也加載不了,也會產生ClassNotFoundException異常并向上拋出
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}
//沒錯!此方法沒有具體實現,只是拋了一個異常,而且訪問權限是protected。這充分證明了:這個方法就是給開發者重寫用的,即自定義類加載器時需實現此方法!
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
  • AppClassLoader,ExtClassLoader 對findClass實現都繼承自URLClassLoader,

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        //將類的包路徑轉換為url資源路徑
                        String path = name.replace('.', '/').concat(".class");
                        //通過URLClassLoader URLClassPath ucp中獲取url路徑對應的二進制數據
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                //讀取二進制獲取調用defineClass獲取Class對象象
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

案例

ClassLoaderTest,EventID,Sdp屬于被不同類加載器管理的類對象,使用相同系統類加載器加載時,由于采用雙親委派機制,獲取Class對象是由不同的類加載器加載。

@Test
    public void classLoaderLoadClass2() throws IOException {
        try {
            //調用加載當前類的類加載器(這里即為系統類加載器)加載ClassLoaderTest
            Class typeLoaded = ClassLoaderTest.class.getClassLoader().loadClass("com.wuhao.jvm.classLoader.ClassLoaderTest");
            //查看被加載的ClassLoaderTest對象是被那個類加載器加載的
            System.out.println(typeLoaded.getClassLoader());

            //調用加載當前類的類加載器(這里即為系統類加載器)加載EventID
            Class typeLoaded1 = ClassLoaderTest.class.getClassLoader().loadClass("com.sun.java.accessibility.util.EventID");
            //查看被加載的ClassLoaderTest對象是被那個類加載器加載的
            System.out.println(typeLoaded1.getClassLoader());

            //調用加載當前類的類加載器(這里即為系統類加載器)加載Sdp
            Class typeLoaded2 = ClassLoaderTest.class.getClassLoader().loadClass("com.oracle.net.Sdp");
            //查看被加載的ClassLoaderTest對象是被那個類加載器加載的
            System.out.println(typeLoaded2.getClassLoader());
        } catch (Exception e) {
        }
    }

模式優點

雙親委派模型很好的解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載).

例如類java.lang.Object,它存在在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。
相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,用戶編寫了一個java.lang.Object的類,并放在程序的ClassPath中,那系統中將會出現多個不同的Object類,程序將混亂。因此,如果開發者嘗試編寫一個與rt.jar類庫中已有類重名的Java類,將會發現可以正常編譯,但是永遠無法被加載運行。

模式缺點

由于雙親委派模型存在,一個類被誰加載是由類加載器管理資源所決定,即使某個類存在于多個類加載器管理資源中。也只會被更上程的類加載器加載。這樣就會導致一個在被上層類加載中Class中獲取的上程類加載器,是無法加載下層類加載器中管理Class對象。

線程上下文類加載器

為了解決這個問題JVM引用了線程上下文類加載器

這個類加載器可以通過java.lang.Thread類的setContextClassLoader方法進行設置。如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過多的話,那這個類加載器默認即使用系統程序類加載器。

如何破壞雙親委派模型

雙親委派模型并不是一個強制性的約束模型,而是Java設計者推薦給開發者的類加載器的實現方式。大多數的類加載器都遵循這個模型,雙親委派模型的具體邏輯實現在ClassLoader的loadClass方法,如果我們自定義類加載器,采用雙親委派模型:只需要重寫ClassLoader的findClass()方法即可,破壞雙親委派模型:重寫ClassLoader的整個loadClass()方法(因為雙親委派模型的邏輯主要實現就在此方法中,若我們重寫即可破壞掉。)

3 tomcat中類加載器

3.1 Tomcat 如果使用默認的類加載機制行不行

Tomcat是個web容器,那么它要解決什么問題

  • 一個web容器可能需要部署多個個應用程序,不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。
  • 部署在同一個web容器中相同的類庫相同的版本可以共享給所有的應用程序。節省資源。
  • web容器也有自己依賴的類庫,不能于應用程序的類庫混淆。基于安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
  • web容器要支持jsp的修改,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行后修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改后不用重啟。

Tomcat 如果使用默認的類加載機制是無法解決上面的問題,

  • 對于第一個問題,如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,默認的累加器是不管你是什么版本的,只在乎你的全限定類名,并且只有一份。
  • 第三個問題和第一個問題一樣。
  • 第四個問題,我們想我們要怎么實現jsp文件的熱部署。jsp 文件其實也就是class文件,那么如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改后的jsp是不會重新加載的。我們只能將jsp文件Class對象從類加載器中卸載,在重新加載。
3.2 Tomcat類加載器
image
  • commonLoader: Tomcat 通用類加載器, 加載的資源可被 Tomcat 和 所有的 Web 應用程序共同獲取
  • catalinaLoader: Tomcat 類加載器, 加載的資源只能被 Tomcat 獲取
  • sharedLoader: Tomcat 各個Context的父加載器, 這個類是所有 WebappClassLoader 的父類, sharedLoader 所加載的類將被所有的 WebappClassLoader 共享獲取
  • WebappClassLoader: 每個Context 對應一個 WebappClassloader, 主要用于加載 WEB-INF/lib 與 WEB-INF/classes 下面的資源

3.3 實例化commonLoader,catalinaLoader,sharedLoader

Bootstrap的初始化中實例化的tomcat類加載器主要包括commonLoader,catalinaLoader,sharedLoader,其本質是URLClassLoader。其核心思想是“根據類加載器的名稱從讀取/conf/catalina.properties中讀取不同類加載加載class文件資源路徑,轉為為URL數組,來實例化成URLClassLoader”

源碼

initClassLoaders方法中構造類加載器被封裝到createClassLoader方法中。第一個參數用來作為需要構造類加載器的名稱,第二個參數用來作為構造加載器的父類加載器。

    private void initClassLoaders() {
        try {
            /** 實例化commonLoader,如果未創建成果的話,則使用應用程序類加載器作為commonLoader **/
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                commonLoader=this.getClass().getClassLoader();
            }
            /** 實例化catalinaLoader,其父加載器為commonLoader  **/
            catalinaLoader = createClassLoader("server", commonLoader);
            /** 實例化sharedLoader,其父加載器為commonLoader  **/
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

createClassLoader方法中根據類加載器的名稱從讀取/conf/catalina.properties中讀取不同類加載加載class文件資源路徑,

將讀取的資源轉換為Repository類型的列表。Repository類用來表示一個資源。其內部存在2個屬性location,location。分別表示資源的路徑和資源的類型

調用ClassLoaderFactory.createClassLoader創建一個ClassLoader

/**
     * 按照名稱創建不同tomcat 類加載器
     * @param name
     * @param parent
     * @return
     * @throws Exception
     */
    private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {

        /** 根據類加載器的名稱從CatalinaProperties配置中讀取不同類加載加載class文件資源路徑,
         *
         * CatalinaProperties配置來源于tomcat工作目錄/conf/catalina.properties
         * 默認配置如下
         * common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
         * server.loader=
         * shared.loader=
         */
        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;

        /** 替換資源路徑字符串中屬性遍歷占位符,比如:${catalina.base}、${catalina.home}  **/
        value = replace(value);

        /**
         * 定義一個Repository類型的列表,
         * Repository類用來表示一個資源。其內部存在2個屬性location,location。分別表示資源的路徑和資源的類型
         **/
        List<Repository> repositories = new ArrayList<>();

        /** 讀取配置中的資源按,分隔字符數組**/
        String[] repositoryPaths = getPaths(value);

        /**  遍歷repositoryPaths  **/
        for (String repository : repositoryPaths) {
            /** 檢查資源路徑是否為URL **/
            try {
                @SuppressWarnings("unused")
                URL url = new URL(repository);
                /** 創建一個Repository,類型為URL添加到repositories **/
                repositories.add(
                        new Repository(repository, RepositoryType.URL));
                continue;
            } catch (MalformedURLException e) {
                // Ignore
            }

            /** 判斷資源是否為某個目錄下所有*.jar文件 **/
            if (repository.endsWith("*.jar")) {
                repository = repository.substring
                    (0, repository.length() - "*.jar".length());

                /** 創建一個Repository,類型為GLOB添加到repositories **/
                repositories.add(
                        new Repository(repository, RepositoryType.GLOB));
            }
            /** 判斷資源是否為某個目錄下.jar文件 **/
            else if (repository.endsWith(".jar")) {
                /** 創建一個Repository,類型為JAR添加到repositories **/
                repositories.add(
                        new Repository(repository, RepositoryType.JAR));
            }
            /** 判斷資源是否目錄 **/
            else {
                /** 創建一個Repository,類型為目錄添加到repositories **/
                repositories.add(
                        new Repository(repository, RepositoryType.DIR));
            }
        }

        //創建一個ClassLoader
        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

    public enum RepositoryType {
        //目錄
        DIR,
        //目錄下*.jar
        GLOB,
        //單個jar
        JAR,
        //URL
        URL
    }

    public static class Repository {
        /**
         *  資源路徑
         */
        private final String location;
        /**
         *  資源類型
         */
        private final RepositoryType type;

        public Repository(String location, RepositoryType type) {
            this.location = location;
            this.type = type;
        }

        public String getLocation() {
            return location;
        }

        public RepositoryType getType() {
            return type;
        }
    }    

createClassLoader方法中遍歷列表中Repository,將其轉換為URL,添加到定義URL集合中。最后將URL集合轉化為數組,構造URLClassLoader。

public static ClassLoader createClassLoader(List<Repository> repositories,
                                                final ClassLoader parent)
        throws Exception {

        if (log.isDebugEnabled())
            log.debug("Creating new class loader");

        /** 定義個URL集合,用來作為構造URLClassLoader時需要URL集合參數 **/
        Set<URL> set = new LinkedHashSet<>();


        /** 讀取遍歷repositories,將資源轉換為URL放入set集合中 **/
        if (repositories != null) {
            for (Repository repository : repositories)  {
                if (repository.getType() == RepositoryType.URL) {
                    /** 通過指定資源路徑構造URL **/
                    URL url = buildClassLoaderUrl(repository.getLocation());
                    if (log.isDebugEnabled())
                        log.debug("  Including URL " + url);
                    set.add(url);
                } else if (repository.getType() == RepositoryType.DIR) {
                    File directory = new File(repository.getLocation());
                    directory = directory.getCanonicalFile();
                    /** 校驗目錄資源 **/
                    if (!validateFile(directory, RepositoryType.DIR)) {
                        continue;
                    }
                    /** 獲取文件目錄URL **/
                    URL url = buildClassLoaderUrl(directory);
                    if (log.isDebugEnabled())
                        log.debug("  Including directory " + url);

                    set.add(url);
                } else if (repository.getType() == RepositoryType.JAR) {
                    File file=new File(repository.getLocation());
                    file = file.getCanonicalFile();
                    /** 校驗jar文件資源 **/
                    if (!validateFile(file, RepositoryType.JAR)) {
                        continue;
                    }
                    /**  獲取jar文件URL **/
                    URL url = buildClassLoaderUrl(file);
                    if (log.isDebugEnabled())
                        log.debug("  Including jar file " + url);
                    set.add(url);
                } else if (repository.getType() == RepositoryType.GLOB) {
                    File directory=new File(repository.getLocation());
                    directory = directory.getCanonicalFile();
                    /** 校驗jar文件資源 **/
                    if (!validateFile(directory, RepositoryType.GLOB)) {
                        continue;
                    }
                    if (log.isDebugEnabled())
                        log.debug("  Including directory glob "
                            + directory.getAbsolutePath());

                    String filenames[] = directory.list();
                    if (filenames == null) {
                        continue;
                    }
                    /** 遍歷目錄中文件,找到jar文件,添加到set中 **/
                    for (int j = 0; j < filenames.length; j++) {
                        String filename = filenames[j].toLowerCase(Locale.ENGLISH);
                        if (!filename.endsWith(".jar"))
                            continue;
                        File file = new File(directory, filenames[j]);
                        file = file.getCanonicalFile();
                        if (!validateFile(file, RepositoryType.JAR)) {
                            continue;
                        }
                        if (log.isDebugEnabled())
                            log.debug("    Including glob jar file "
                                + file.getAbsolutePath());
                        URL url = buildClassLoaderUrl(file);
                        set.add(url);
                    }
                }
            }
        }

        /** 將集合URL轉換為數組 **/
        final URL[] array = set.toArray(new URL[set.size()]);
        if (log.isDebugEnabled())
            for (int i = 0; i < array.length; i++) {
                log.debug("  location " + i + " is " + array[i]);
            }

        /** 創建URLClassLoader **/
        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            return new URLClassLoader(array);
                        else
                            return new URLClassLoader(array, parent);
                    }
                });
    }

參考鏈接

Tomcat源碼學習--WebAppClassLoader類加載機制

Tomcat 源碼分析 WebappClassLoader 分析 (基于8.0.5)

深入理解Tomcat(五)類加載機制

深入理解Java類加載器

雙親委派模型與線程上下文類加載器

深入理解 Tomcat(四)Tomcat 類加載器之為何違背雙親委派模型

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容