作為一種即時編譯的程式設計語言,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類載入器