類加載器基本概念
顧名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之后就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class類的一個實例。
任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機中的唯一性。
類加載器類型
- 啟動類(引導類)加載器 Bootstrap ClassLoader, 虛擬機的一部分,由c++實現。負責加載<JAVA_HOME>/lib下的類庫
- 擴展類加載器 Extension ClassLoader, sun.misc.Launcher$ExtClassLoader.負載加載<JAVA_HOME>/lib/ext下的類庫
- 應用程序類加載器 Application ClassLoader ,sun.misc.Launcher$AppClassLoader, 它是System.getClassLoader()的返回值,也稱為系統類加載器。負責加載用戶類路徑上所指定的類庫。如果應用程序沒有自定義過類加載器,一般它就是默認的類加載器。Thread.currentThread.getContextClassLoader,如果沒有setContextClassLoader,默認也是它。
雙親委派模型

雙親委派模型的過程:如果一個類加載器收到了類加載的請求,首先不會自己去加載,而是把請求為派給自己的父類加載器去完成,每一個層次的類加載器都是如此。因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中。只有當父類反饋自己無法完成這個加載請求時,子類加載器才會嘗試自己完成。
好處就是:Object類在程序的各種類加載器環境中都是同一個類。不會造成混亂。
破壞雙親委派模型
雙親委派模型只是推薦,而非強制。有三次大規模破壞該模型的情況。
- JDK 1.0->1.2 , loadClass() -> findClass()
- 模型缺陷:JNDI服務,SPI擴展類是由廠商自己實現,而啟動類加載又不可能認識這些類。只好引入 線程上下文 類加載器Thread Context ClassLoader. 該類加載器可以通過setContextClassLoader()設置,如果創建線程時未設置,將會從父線程繼承。如果在應用的全局范圍內都沒有設置,那就默認是AppClassLoader. 有了這個,JNDI服務就可以去加載所需的SPI擴展代碼,也就是父類加載器請求子類加載器去完成類加載的動作。這其實也就違背了雙親委派模型的一般性原則,但無可奈何。
- 程序動態性的追求: “熱替換” , OSGI. JSR-291
開發自己的類加載器
繼承 java.lang.ClassLoader,覆蓋findClass(String name)即可。
java.lang.ClassLoader類的方法
loadClass()封裝了前面提到的代理模式的實現。該方法會首先調用
findLoadedClass()方法來檢查該類是否已經被加載過;如果沒有加載過的話,會調用父類加載器的
loadClass()方法來嘗試加載該類;如果父類加載器無法加載該類的話,就調用
findClass()方法來查找該類。因此,為了保證類加載器都正確實現代理模式,在開發自己的類加載器時,最好不要覆寫
loadClass()方法,而是覆寫
findClass()方法。
示例:
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"; } }
