ClassLoader 學習筆記

來源:互聯網
上載者:User
        作為一種即時編譯的程式設計語言,ClassLoader是Java程式啟動並執行基礎。雖然,大部分和我一樣的攻城獅平時都不需要和ClassLoader打交道,但是相信大家對於ClassNotFoundExecption和NoClassDefFoundError多多少少有些印象。這兩個類都和ClassLoader關係密切。
        首先,java虛擬機器實現了三個類載入器:        1. 啟動(Bootstrap)類載入器: 負責載入<java_runtime_home>/lib下的類庫,由本地代碼實現,主要用於初始化java虛擬機器,屬於java虛擬機器本地實現的部分。        2. 標準擴充(Extension)類載入器:負責載入<java_runtime_home>/lib/ext下的類庫,由java實現,開發人員可以使用該載入器。        3. 系統(system)類載入器:負責載入CLASSPATH路徑下的類庫,通常開發人員自己編寫的類庫也由該載入器負責載入(bin檔案夾也在CLASSPATH路徑中),可以由ClassLoader.getSystemClassLoader()擷取到該載入器的索引。        此外,開發人員自己編寫的類載入器為自訂類載入器。
        第二,雙親委派機制:        除了,啟動類載入器,其他三類(標準擴充類載入器,system類載入器以及自訂載入器)都直接或間接繼承自java.long.ClassLoader。        以下為ClassLoader的部分實現代碼(JDK1.6):        
    // The parent class loader for delegation    private ClassLoader parent;    /**     * Creates a new class loader using the specified parent class loader for     * delegation.     *     * <p> If there is a security manager, its {@link     * SecurityManager#checkCreateClassLoader()     * <tt>checkCreateClassLoader</tt>} method is invoked.  This may result in     * a security exception.  </p>     *     * @param  parent     *         The parent class loader     *     * @throws  SecurityException     *          If a security manager exists and its     *          <tt>checkCreateClassLoader</tt> method doesn't allow creation     *          of a new class loader.     *     * @since  1.2     */    protected ClassLoader(ClassLoader parent) {        this(checkCreateClassLoader(), parent);    }    /**     * Creates a new class loader using the <tt>ClassLoader</tt> returned by     * the method {@link #getSystemClassLoader()     * <tt>getSystemClassLoader()</tt>} as the parent class loader.     *     * <p> If there is a security manager, its {@link     * SecurityManager#checkCreateClassLoader()     * <tt>checkCreateClassLoader</tt>} method is invoked.  This may result in     * a security exception.  </p>     *     * @throws  SecurityException     *          If a security manager exists and its     *          <tt>checkCreateClassLoader</tt> method doesn't allow creation     *          of a new class loader.     */    protected ClassLoader() {        this(checkCreateClassLoader(), getSystemClassLoader()); //預設情況下會使用SystemClassLoader作為parent    }
    /**     * 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:     *     * <p><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>     *     * @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 synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException    {// First, check if the class has already been loadedClass c = findLoadedClass(name); //尋找已經載入的類,如果已經載入過,則沒必要再次載入
                                         //如果移除這個檢查,則可能會因重複定義而拋出LinkageErrorif (c == null) {    try {if (parent != null) {     //先委託父載入器載入class    c = parent.loadClass(name, false);} else {                  //如果parent == null, 則我們認為父載入器為啟動類載入器    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.        c = findClass(name); //如果父類載入器未能完成載入,則由子類具體實現    }}if (resolve) {    resolveClass(c);// 進行連結}return c;    }

        

        從loadClass函數的實現,我們發現載入器進行載入時,首先會委託父類載入器嘗試載入,這個就是雙親委派機制。

        四種類型的父子關係如,從代碼上來說,BootStrapClassLoader並不是ExtensionClassLoader的parent(實際上BootStrapClassLoader為本地語言實現,所以ExtensionClassLoader的parent=null),但是從邏輯上來說,parent=null等效於parent=BootStrapClassLoader。        以下是四類類載入器的父子關係:        

        所以,我們可以通過ClassLoader.getSystemClassLoader().getParent()來獲得ExtensionClassLoader的索引。                值得一提的是,雙親委派機制能較好的滿足大部分java應用的需求。但是,在一些特殊情境下,也有為了提升效能而修改雙親委派機制的情況,例如:先由當前載入器載入類,載入失敗再委託父類載入器載入。                第三,每一個ClassLoader都對應一個命名空間,而jvm通過這個命名空間+class的全名(包括package name)唯一的標識一個類。所以,有幾個問題需要注意:       1. 同一個類其實是可以重複載入的,如果你使用兩個不同的類載入器來load同一個class,則可以在jvm存在兩個完全相同的類定義(它們的命名空間不同)。       2. 由第一問題,引申出來的問題,同一個類經由不同的類載入器載入,對於JVM來說就是不同的類,所以,可能發生ClassCastException,例如:      

 ClassA a = new ClassA();    // ClassA經由ClassLoader1載入ClassA b = null; // ClassA經由ClassLoader2載入b = (ClassA)a; //拋出ClassCastException

    3. 因為雙親委派機制的存在,所以,執行ClassLoaderA.loadClass("com.example.classA")載入的classA並不一定是由ClassLoaderA載入的,也有可能是由其父載入器載入的,例如(SystemClassLoader)。這種情況下,ClassLoaderA被成為classA的初始類載入器,而SystemClassLoader為定義類載入器,類的命名空間由定義類載入器。         第四,java動態載入       當項目存在一些特殊需求,例如:        1. app運行時需要從網路擷取最近的class/jar檔案,動態更新        2. app有較高的安全需求,對class/jar檔案做了額外的加密操作,使通常的類載入器無法解析class/java檔案        等特殊情況,就需要考慮動態載入.        常用的java動態載入方案有Class.forName和自訂類載入器。
        利用Class.forName實現動態載入的常見案例是JDBC驅動的載入。
        Class.forName函數有兩種重載:        

    public static Class<?> forName(String className)                 throws ClassNotFoundException {        return forName0(className, true, ClassLoader.getCallerClassLoader());//預設情況下調用Class.forName函數的調用者的ClassLoader進行載入(classloader參數=ClassLoader.getCallerClassLoader()),並完成串連和初始化(initialize參數=true)。    }    public static Class<?> forName(String name, boolean initialize,   ClassLoader loader)        throws ClassNotFoundException    {if (loader == null) {    SecurityManager sm = System.getSecurityManager();    if (sm != null) {ClassLoader ccl = ClassLoader.getCallerClassLoader();if (ccl != null) {    sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);}    }}return forName0(name, initialize, loader);    }

              使用自訂類載入器實現動態載入的常見案例是,利用實現代碼的動態更新(或許這個特性未來會在雲OS這種特定平台上大展身手)。       其實理論上來說,標準擴充類載入器和系統類別載入器也可以實現動態載入,但是,一般來說,這兩個載入器內類掃描路徑(<java-runtime-home>/lib/ext,以及CLASSPATH)下的檔案不會變動,所以一般都以靜態方式使用。       根據sun的建議實現一個自訂類載入器還是比較簡單的。 繼承ClassLoader,並覆蓋findClass函數(在sun的標準實現中,標準擴充類載入器和系統類別載入器都繼承自URLClassLoader,它是ClassLoader的一個子孫類,所以,個人覺得自訂類載入器繼承自URLClassLoader也是各不錯的選擇):以下代碼摘錄自:http://www.ibm.com/developerworks/cn/java/j-lo-classloader/

public class FileSystemClassLoader extends ClassLoader {     private String rootDir;     public FileSystemClassLoader(String rootDir) {         this.rootDir = rootDir;     }     protected Class<?> findClass(String name) throws ClassNotFoundException {         byte[] classData = getClassData(name);         if (classData == null) {             throw new ClassNotFoundException();         }         else {             return defineClass(name, classData, 0, classData.length);         }     }     private byte[] getClassData(String className) {         String path = classNameToPath(className);         try {             InputStream ins = new FileInputStream(path);             ByteArrayOutputStream baos = new ByteArrayOutputStream();             int bufferSize = 4096;             byte[] buffer = new byte[bufferSize];             int bytesNumRead = 0;             while ((bytesNumRead = ins.read(buffer)) != -1) {                 baos.write(buffer, 0, bytesNumRead);             }             return baos.toByteArray();         } catch (IOException e) {             e.printStackTrace();         }         return null;     }     private String classNameToPath(String className) {         return rootDir + File.separatorChar                 + className.replace('.', File.separatorChar) + ".class";     }  }
        ClassLoader中比較值得注意的函數有:        1. loadClass函數,此函數實現了已載入類的檢查和雙親委派機制的實現。沒有不要的情況下,不要覆蓋這個函數。        2. findClass函數,此函數由loadClass函數調用,負責尋找需要載入的類對應的位元組碼(.class檔案的內容),如果找不到對應的位元組碼,則拋出ClassNotFoundException。        3. defineClass,此函數由findClass函數調用,負責解析位元組碼進而產生對應的類。如果解析失敗,則拋NoClassDefFoundError。不建議覆蓋。        java動態載入雖然帶來了優勢,可以讓開發人員實現很多功能,但是,也存在一定的副作用,因為app的局部是動態變化的,那麼靜態部分就無法使用通常的方式來調用動態更新的類。所以,一般需要通過如下兩種方式來調用:       1. 介面,動態載入的類始終實現指定的介面,靜態部分通過介面來調用動態載入的類。       2. 反射(Reflect),通過指定的類名、函數名(成員名)來調用動態載入的類。               第五,線程上下文類載入器        線程上下文下載器可以由Thread.setContextClassLoader函數和Thread.getContextClassLoader函數設定和擷取。線程上下文類載入器可以是任何繼承自ClassLoader的載入器執行個體,可以是系統類別載入器,也可以是自訂類載入器。預設情況下,線程會繼承其父線程的ContextClasLoader,而java初始線程的ContextClassLoader為系統類別載入器,所以,在未設定的情況下,所有的線程上下文類載入器為系統類別載入器。              java為了提高開放性,提供了很多服務提供者介面(Service Provider Interface,SPI)。而雙親委派機制在這些情形下出現了問題。以JAXP(java xml 解析API)為例: javax.xml.parsers(JAXP的SPI介面)定義由java核心庫提供,由啟動類載入器負責載入,而對應的實現 Apache
xercers則存在於ClassPath路徑下,由系統類別載入器負責載入。根據class.forName函數的預設實現規則,java.xml.parsers包內的類會使用啟動類載入器去載入Apache的實作類別,但是啟動類載入器為系統類別載入器的父類載入器,啟動類載入器不會,也無法調用系統載入器,所以,會導致載入失敗。
       為瞭解決這些問題,從java1.2開始引入了上下文載入器,以便SPI介面載入其實作類別。
       第六,java位元組碼的格式       ClassLoader內的findClass最終由native代碼實現,我一直無緣得見如何將.class檔案解析為class,所以去查詢了一些其他的資料。
       java位元組碼的格式在《jvm虛擬機器規範》中有描述。       
ClassFile {     u4 magic;          // magic number, 固定為0xCAFEBABE    u2 minor_version;  // 子版本號碼, 一般為0x0    u2 major_version;  // 主要版本號, 由java的版本號碼決定,java1.5=0x31, java1.6=0x32, java1.7=0x33    u2 constant_pool_count; //常量池長度    cp_info constant_pool[constant_pool_count-1];  //常量池    u2 access_flags;  // 訪問標誌,private, package, public...    u2 this_class; // 本類,為常量池內的有效索引    u2 super_class; // 超類,為常量池內的有效索引    u2 interfaces_count; // 實現的介面數量    u2 interfaces[interfaces_count]; // 實現的介面池    u2 fields_count; field_info fields[fields_count]; // 成員數量以及成員池     u2 methods_count; method_info methods[methods_count]; // 方法數量及方法池    u2 attributes_count; attribute_info attributes[attributes_count]; //屬性數量及屬性池}


        詳情請見:《java虛擬機器規範》閱讀(三) class檔案格式        第七, 類載入之後發生的事情        類載入成功之後,jvm會開始執行連結操作(ClassLoader.resolveClass()就是連結一個類)。        連結操作分為如下三個小步驟:         1. 檢驗:檢驗操作是為了保證java位元組碼是正確的。.class檔案可能本身不是有效檔案(例如空檔案,或者由.mp3檔案修改尾碼而來),也有可能是因為java版本不符(java1.5的虛擬機器無法解析java1.6的class檔案)。如果驗證成功則繼續連結,否則拋出java.lang.VerifyError錯誤。         2. 準備:為類的靜態變數分配記憶體空間,初始化預設值。         3. 解析:需要連結的java類一般都會包含對於其他類和介面的引用(包括其父類,實現的介面,方法的參數、傳回值所涉及到的類)。解析的目的就是為了保證這些類可以被找到。常用的解析策略包括遞迴解析和用時解析。遞迴解析的就是遞迴載入依賴的介面和類。而更常用的策略則是用時解析,當真正需要使用這個類的時候在進行解析。                連結成功後,jvm接下來會進行初始化。初始化操作主要是執行靜態代碼塊和初始化靜態域。        初始化靜態域和連結操作中的準備步驟不一樣。以如下代碼為例:

private static int number = 1;

       準備步驟進行的操作,其實在heap上分配4個位元組的空間,並在將其初始化為0;

       而初始化靜態域則是把剛才剛才分配的空間設定為1。              執行靜態代碼塊,這是class檔案內的代碼第一次被執行。       總結,經過jvm載入,連結,初始化三個步驟的操作,一個class檔案轉變為java.long.Class的子類,可以在jvm中執行。參考資料:java類載入原理解析深入探討java類載入器

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.