深入JAVA虛擬機之類加載器

前言:
虛擬機設計團隊把類加載階段中"通過一個類的權限定名來獲取描述此類的二進制字節流"這個動作放到虛擬機外部區實現,讓程序自己決定如何去獲取所需的類。實現這個動作的代碼模塊就被稱爲類加載器

它最初是爲了滿足Java Applet的需求而被開發,而現在Java Applet基本已經宣佈死亡,但類加載器卻在類層次劃分、OSGi、熱部署、代碼加密等領域大放異彩。它是java技術體系中一塊重要的基石。


對於任意一個類,都需要由加載該類的加載器和這個類本身一同確立在虛擬機中的唯一性。通俗說就是:比較兩個類是否相等,這兩個類必須是同一個類加載器的前提纔有意義。否則,就算兩個類是來源同一個class文件,只要加載器不同,那麼這兩個類必定是不相等的。

備註:
這裏所指的"相等", 包括:Class對象的equals方法、isAssignableForm方法、isInstance方法的返回結果,也包括instanceof關鍵字做對象所屬關係判定等情況。

雙親委派模型

在虛擬機的角度看,只有兩類類加載器:啓動類加載器、其他類加載器。啓動類加載器是屬於虛擬機中的一部分,是用C++實現的,而其他類加載器則是java實現的。在開發者眼中看來,類加載器可以分的更加細緻些:
啓動類加載器:
這個類加載器,負責把放在JAVA_HOME\lib目錄中的,或者被-Xbootclasspath參數指定的路徑中的,並且是虛擬機識別的類庫加載到虛擬機中。啓動類加載器無法被java程序直接引用。

擴展類加載器:
它是負責JAVA_HOME\lib\ext目錄中的,或者被java.ext.dirs系統變量指定的所有類庫,開發者可以直接使用擴展類加載器。

應用程序類加載器:
這個類加載器是由sun,misc.Launcher$AppClassLoader實現的。因爲這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般都會稱爲系統類加載器。它負責加載用戶類路徑上的(ClassPath)上所指定的類庫。開發者可以直接使用這個類加載器。如果應用程序中沒有自定義過自己的類加載器,一般這個就是程序中默認的類加載器。

備註:
我們的應用程序都是由這三種類加載器相互配合進行加載的,如果有必要,還可以加入自己的類加載器。類加載器與自定義類加載器之間的關係如下圖:
深入JAVA虛擬機之類加載器

雙親委派模型,規定了,每一個類加載器(除了啓動類加載器)都必須有父類,並且這種關係不是繼承實現,而是通過組合實現的,而且要求當調用了當前類加載器時,必須先給父類去加載,如果沒有父類,那麼就交給啓動類加載器加載,如果父類返回無法加載,則給當前類加載器加載,如果當前也無法加載,則拋出ClassNotFoundException.

這樣做的好處,就會有一個優先級,確保一個類只會被一個類加載器加載如虛擬機中,如果都是優先當前,那麼就會可能出現同一個類在虛擬機中被不同的類加載器加載了多遍,並且這多個類還是不相等的。

雙親委派的代碼都在java.lang.ClassLoader的loadClass方法中,我們可以通過分析源代碼就可以知道:

/**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     *
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
     * during the entire class loading process.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

破壞雙親委派模型
雙親委派模型不是一個強制性的約束模型,只是java設計者們推薦給開發者的類加載器實現方式。絕大部分都是遵循這個模型的。但截止到深入java虛擬機作者編寫這本書時就已經有三次破壞該模型。

第一次:
是出現在雙親委派模型之前,因爲jdk1.2之後才引入雙親委派模型。而類加載器和抽象類java.lang.ClassLoader早在jdk1.0就出現了。爲了兼容雙親委派模型之前的類加載器,java設計團隊不得不妥協,給jdk1.2之後的java.lang.ClassLoader類新添加了一個protected的findClass方法,在此之前,用戶去繼承java.lang.ClassLoader就是爲了重寫loadClass()方法,因爲虛擬機在進行類加載的時候會調用類加載器的 一個私有方法loadClassInternal(),而此方法唯一邏輯就是調用自己的loadClass()方法。前面通過loadClass源碼,我們可以知道如果父類加載失敗,纔會去調用findClass()方法定義的類加載過程。這樣就可以保證滿足雙親委派模型。

第二次:
第二次是因爲雙親模型自身的缺陷,雖然雙親委派模型很好的解決了各個類加載器的基礎類的統一,但如果基礎類又要調用回用戶的代碼呢?比如說:JDNI服務,JDNI服務是java的標準服務,它的代碼是有啓動類加載器加載,但JDNI的目的是爲了對資源進行集中管理和查找,它需要調用其他獨立廠商實現並部署在應用程序的ClassPath下的JDNI接口提供者的代碼,但啓動類並不認識這些代碼。爲了解決這類問題,java設計團隊,提供了一種並不優雅的設計:線程上下文類加載器。這個類加載器可以通過java.lang.Thread類的setContextCLassLoader()方法進行設置,如果沒有創建線程時設置,它會從父線程中繼承一個,如果應用程序全局都沒有設置,那麼這個類加載器就是應用程序類加載器。

JDNI服務使用了這個線程上下文加載器去加載所需的SPI代碼,也就是父類加載器請求子類加載器去完成類加載動作。java中設計SPI的加載動作基本都採用這種方式,比如:JDNI、JDBC、JAXB、JCE和JBI等。

第三次:
第三次是由於用戶對程序動態性的追求導致的,這裏說的動態性是指:代碼熱替換、模塊熱部署等,說白了就是希望應用程序能像電腦外設那樣,插上鼠標,鍵盤,不用重啓機器。熱部署對於企業生產環境的誘惑是毋庸置疑的。
深入JAVA虛擬機之類加載器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章