1. 程式人生 > >JVM學習記錄-類加載器

JVM學習記錄-類加載器

img was ear 組織 lib wing bee return rem

前言

JVM設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作房東Java虛擬機外面去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱為“類加載器”。

類與類加載器

類加載器雖然只用戶實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。每個類都有一個獨立的類名稱空間,在比較兩個類是否“相等”,只有兩個類是由同一個類加載器加載的前提下才有意義,否則即使兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

這裏的相等,包含Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關系判定等情況。例如如下代碼:

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception{

        ClassLoader myClassLoader = new ClassLoader() {
            /**
             * Loads the class with the specified <a href="#name">binary name</a>.
             * This method searches for classes in the same manner as the {
@link * #loadClass(String, boolean)} method. It is invoked by the Java virtual * machine to resolve class references. Invoking this method is equivalent * to invoking {@link #loadClass(String, boolean) <tt>loadClass(name, * false)</tt>}. * *
@param name The <a href="#name">binary name</a> of the class * @return The resulting <tt>Class</tt> object * @throws ClassNotFoundException If the class was not found */ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try{ String fileName = name.substring(name.lastIndexOf(".")+1)+".class"; InputStream inputStream = getClass().getResourceAsStream(fileName); if(null == inputStream){ return super.loadClass(name); } byte[] b = new byte[inputStream.available()]; inputStream.read(b); return defineClass(name,b,0,b.length); }catch (IOException e){ throw new ClassNotFoundException(name); } } }; Object obj = myClassLoader.loadClass("com.eurekaclient2.client2.shejimoshi.JVM.ClassLoaderTest").newInstance(); System.out.println("來源:"+obj.getClass()); System.out.println(obj instanceof com.eurekaclient2.client2.shejimoshi.JVM.ClassLoaderTest); } }

運行結果:

來源:class com.eurekaclient2.client2.shejimoshi.JVM.ClassLoaderTest
false

從運行結果中我們可以看出來,obj對象確實屬於ClassLoaderTest類的對象,但是從運行結果的第二行中可以看出來,這個對象與ClassLoaderTest類做所屬類型檢查時返回的false,因為虛擬機中存在了兩個ClassLoaderTest類,一個是由系統應用程序類加載器加載的,另一個是由我們自定義的類加載器加載的,雖然都來自同一個Class文件,但依然是兩個獨立的類。

雙親委派模型

從虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader);另一種就是所有其他的類加載器,這些類加載器都是由Java語言實現,並且獨立於虛擬機外部,並都繼承自抽象類java.lang.ClassLoader。

從Java開人員的角度來看,類加載器可以分的更細一些,但是絕大部分java程序都會用到下面的這3種系統提供的類加載器。

啟動類加載器(Bootstrap ClassLoader):它負責將放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中,並且是虛擬機識別的類庫加載到虛擬機中。

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

應用程序類加載器(Application ClassLoader):它一般被稱為系統類加載器,負責加載用戶類路徑上所指定的類庫,開發者可以直接使用這個類加載器,若應用程序中沒有自定義過類加載器,一般情況下默認的就是這個應用程序類加載器。

我們的應用程序都是由這3種類加載器相互配合進行加載的,如果有需要,還可以加入自定義的類加載器。這些類加載器的關系如下圖:

技術分享圖片

類加載器的之間的這種層次關系,稱為類加載器的雙親委派模式(Parents Delegation Model)。這種模型要求,除了頂層外,其余的類加載器都應當有自己的的父類加載器。這裏的子父關系一般不會以繼承方式來實現,而是使用組合關系來復用父加載器的代碼。

雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是這樣,最終都應該傳送到頂層的啟動類加載器中,只有當父類反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載。

使用雙親委派模型來組織類加載器之間的關系,有一個顯而易見測好處就是Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。

記得以前看到過一個面試題,題目大概意思是:能不能自己寫一個類叫java.lang.String?

答案:不可以。原因就是因為JVM的類加載器采用的這種雙親委派模型,當我們寫了一個類叫java.lang.String時,類加載器發現已經加載過一個同樣的類了,不用加載了,直接使用就可以了。所以自己寫的這個java.lang.String這個類可以編譯通過,但是無法被加載運行。

實現雙親委派的代碼集中在java.lang.ClassCloader的loadClass()方法中,邏輯很簡單:首先檢查自己是否已經被加載過,如果沒有加載則調用父加載器的loadClass()方法,若父加載器為空則默認使用啟動類加載器作為父加載器。如果父加載器加載失敗,則拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。

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設計者推薦給開發者的類加載器的實現方式。在Java世界中大部分的類加載器都遵循這個模型,但也有例外,雙親委派模型到目前為止主要出現過3次較大規模的“被破壞”情況。

  • 第一次:由於類加載器是JDK1.0就已經存在了,而雙親委派模型在JDK1.2之後才被引入,所以為了向前兼容,做了一些妥協。在JDK1.2以後已不再提倡用戶去覆蓋loadClass()方法,而應該把自己的實現邏輯寫在findClass()方法中,這樣在loadClass方法中如果父類加載器加載失敗,就會調用自己的findClass方法來完成加載,這樣就保證了自己實現的類加載器符合雙親委派模型了。
  • 第二次:雙親委派模型的規則是自低向上(由子到父)來進行加載的,但是有些情況下父類是需要調用子類的代碼,這種情況就需要破壞這個模型了。為了解決這種情況,Java設計團隊,引入了一個新的加載器:線程上下文加載器(Trhead Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setCOntextClassLoader()方法進行設置,通過getContextClassLoader()方法來獲得。如果創建線程時還未設置,它會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器就是應用程序類加載器了。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如:JNDI、JDBC、JCE、JAXB、和JBI等。其實我們常用的Tomcat這種應用服務器也是使用的這種類加載器。
  • 第三次:為了實現熱部署,熱插拔,模塊化等功能。就是說更新了一些模塊而不需要重啟,只需要把類和類加載器一同替換掉就可以實現熱部署了。

JVM學習記錄-類加載器