類加載器的基本概念 java
類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。通常來講,Java 虛擬機使用 Java 類的方式以下:Java 源程序(.java 文件)在通過 Java 編譯器編譯以後就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class類的一個實例。每一個這樣的實例用來表示一個 Java 類。經過此實例的 newInstance()方法就能夠建立出該類的一個對象。 web
基本上全部的類加載器都是 java.lang.ClassLoader類的一個實例。 數據庫
下面詳細介紹這個 Java 類。 apache
java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,而後從這些字節代碼中定義出一個 Java 類,即 java.lang.Class類的一個實例。除此以外,ClassLoader還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。爲了完成加載類的這個職責,ClassLoader提供了一系列的方法,比較重要的方法如 表 1所示。關於這些方法的細節會在下面進行介紹。
表 1. ClassLoader 中與加載類相關的方法 api
方法 |
說明 |
getParent() |
返回該類加載器的父類加載器。 |
loadClass(String name) |
加載名稱爲 name的類,返回的結果是 java.lang.Class類的實例 |
findClass(String name) |
查找名稱爲 name的類,返回的結果是 java.lang.Class類的實例 |
findLoadedClass(String name) |
查找名稱爲 name的已經被加載過的類,返回的結果是 java.lang.Class類的實例 |
defineClass(String name, byte[] b, int off, int len) |
把字節數組 b中的內容轉換成 Java 類,返回的結果是 java.lang.Class類的實例。這個方法被聲明爲 final的 |
resolveClass(Class<?> c) |
連接指定的 Java 類 |
1.分類
1.1.Bootstrap ClassLoader(啓動類加載器)
加載JAVA_HOME/lib目錄下的核心api 或 -Xbootclasspath 選項指定的jar包裝入工做,
是用原生代碼來實現的, 並不繼承自 java.lang.ClassLoader。
1.2.Extension ClassLoader(擴展類加載器)
加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄
加載JAVA_HOME/lib/ext目錄下的jar包或 -Djava.ext.dirs 指定目錄下的jar包
1.3.System ClassLoader(系統類加載器)
加載java -classpath/-cp/-Djava.class.path所指的目錄下的類與jar包
它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。
能夠經過 ClassLoader.getSystemClassLoader()來獲取它。
每一個classpath以文件名或目錄結尾,該文件名或目錄取決於將類路徑設置成什麼:
對於包含.class文件的.zip或.jar文件,路徑以.zip或.jar文件名結尾。
對於未命名包中的.class文件,路徑以包含.class文件的目錄結尾。
對於已命名包中的.class文件,路徑以包含root包(完整包名中的第一個包)的目錄結尾。
數組
1.4.自定義類加載器
經過繼承 java.lang.ClassLoader類的方式實現本身的類加載器,
用戶自定義 ClassLoader 能夠根據用戶的須要定製本身的類加載過程,在運行期進行指定類的動態實時加載。
tomcat
2. 層次結構 安全
這四種類加載器的層次關係如上圖所示。
通常來講,這四種類加載器會造成一種父子關係,高層爲低層的父加載器。
能夠經過如下代碼來獲取類加載器, 同時該代碼也演示了類的層次結構 網絡
public class ClassLoaderTree { public static void main(String[] args) { ClassLoader loader = ClassLoaderTree.class.getClassLoader(); while (loader != null) { System.out.println(loader.toString()); loader = loader.getParent(); } } }代碼運行結果以下:
sun.misc.Launcher$AppClassLoader@9304b1 sun.misc.Launcher$ExtClassLoader@190d11第一個輸出的是 ClassLoaderTree類的類加載器, 即系統類加載器。它是 sun.misc.Launcher$AppClassLoader類的實例
3.加載過程 app
在進行類加載時,首先會自底向上挨個檢查是否已經加載了指定類,若是已經加載則直接返回該類的引用。
若是到最高層也沒有加載過指定類,那麼會自頂向下挨個嘗試加載,直到用戶自定義類加載器,若是還不能成功,就會拋出異常。
直接使用系統加載器加載類失敗拋出的是NoClassDefFoundException異常。
若是使用自定義的類加載器loadClass方法或者ClassLoader的findSystemClass方法加載類,拋出的是 ClassNotFoundException。
如下代碼是除 BootstrapClassLoader 外的類加載器加載流程:
// 檢查類是否已被裝載過 Class c = findLoadedClass(name); if (c == null ) { // 指定類未被裝載過 try { if (parent != null ) { // 若是父類加載器不爲空, 則委派給父類加載 c = parent.loadClass(name, false ); } else { // 若是父類加載器爲空, 則委派給啓動類加載加載 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 啓動類加載器或父類加載器拋出異常後, 當前類加載器將其 // 捕獲, 並經過findClass方法, 由自身加載 c = findClass(name); } }4.加載類時的幾個原則
4.1. 代理/雙親委託
類加載器在嘗試本身去查找某個類的字節代碼並定義它時, 會先代理給其父類加載器, 父加載器也會請求它的父加載器代理加載, 依次類推。
在介紹代理模式背後的動機以前, 首先須要說明一下 Java 虛擬機是如何斷定兩個 Java 類是相同的。
Java 虛擬機不只要看類的全名是否相同, 還要看加載此類的類加載器是否同樣。
只有二者都相同的狀況, 才認爲兩個類是相同的。
即使是一樣的字節代碼, 被不一樣的類加載器加載以後所獲得的類,也是不一樣的。
下面經過實例代碼來講明:
package com.example; public class Sample { private Sample instance; public void setSample(Object instance) { this.instance = (Sample) instance; } }測試Java類是否相同:
public class ClassIdentity { public static void main(String[] args) { new ClassIdentity().testClassIdentity(); } public void testClassIdentity() { String classDataRootPath = "C:\\Documents and Settings\\Administrator\\workspace\\Classloader\\classData"; FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); String className = "com.example.Sample"; try { Class class1 = fscl1.loadClass(className); Object obj1 = class1.newInstance(); Class class2 = fscl2.loadClass(className); Object obj2 = class2.newInstance(); Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); setSampleMethod.invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } } }
測試Java類是否相同的代碼運行結果:
java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26) at classloader.ClassIdentity.main(ClassIdentity.java:9) Caused by: java.lang.ClassCastException: com.example.Sample cannot be cast to com.example.Sample at com.example.Sample.setSample(Sample.java:7)運行結果能夠看到,運行時拋出了 java.lang.ClassCastException異常。
由於在此模型下用戶自定義的類裝載器不可能裝載應該由父加載器裝載的可靠類,從而防止不可靠甚至惡意的代碼代替由父加載器裝載的可靠代碼。
例如全部 Java 應用都至少須要引用 java.lang.Object類,也就是說在運行的時候,java.lang.Object這個類須要被加載到 Java 虛擬機中。
若是這個加載過程由 Java 應用本身的類加載器來完成的話,極可能就存在多個版本的 java.lang.Object類,並且這些類之間是不兼容的。
經過代理模式,對於 Java 核心庫的類的加載工做由引導類加載器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。
可是實際上,類加載器的編寫者能夠自由選擇不用把請求委託給parent加載器,也就是能夠違背代理原則, 但正如上所說,會帶來安全的問題。
4.2. 可見性/隔離性
被子加載器加載的類擁有被父加載器加載的類的可見性,但反之則否則。
自定義類加載器擁有三個其本類加載器加載的全部類的可見性,可是處於不一樣分支的自定義類加載器相互之間不具備可見性。
所謂不可見即不能直接互相訪問, 也就是即便它們裝載同一個類,也會擁有不一樣的命名空間, 會有不一樣的Class實例。
但若是持有類所對應的Class對象的引用, 仍是能夠訪問另外一命名空間的類。正如示例代碼中咱們經過反射的方式實現了不一樣加載器加載的類的訪問。
Class<?> class1 = fscl1.loadClass(className); Object obj1 = class1.newInstance(); Class<?> class2 = fscl2.loadClass(className); Object obj2 = class2.newInstance(); Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); setSampleMethod.invoke(obj1, obj2);
一樣咱們也能夠利用可見性原則實現不一樣加載器加載的類之間的互訪, 只須要對Sample類稍加改造, 讓其實現ISample接口。
public interface ISample { public void setSample(Object instance) }
public class Sample implements ISample { private Sample instance; public void setSample(Object instance) { this.instance = (Sample) instance; } }
String classDataRootPath = "C:\\Documents and Settings\\Administrator\\workspace\\Classloader\\classData"; FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); String className = "com.example.Sample"; try { Class<?> class1 = fscl1.loadClass(className); ISample obj1 = (ISample)class1.newInstance(); Class<?> class2 = fscl2.loadClass(className); ISample obj2 = (ISample)class2.newInstance(); obj1.setSample(obj2); } catch (Exception e) { e.printStackTrace(); }
上面示例的代碼中咱們使用自定義類加載器加載了Sample類, 而接口ISample是由系統類加載器加載的, 因此ISample對於Sample是具備可見性的, 所以轉型成功。
4.3. 惟一性咱們繼續分析上面的示例, 使用下面的代碼作轉型
Class<?> class1 = fscl1.loadClass(className); Sample obj1 = (Sample)class1.newInstance();
若是咱們嘗試直接使用如上的代碼來訪問, 會拋出 ClassCastException 異常。
由於在 Java 中, 即便是同一個類文件,若是是由不一樣的類加載器加載的, 那麼它們的類型是不相同的。
在上面的例子中class1是由自定義類加載器加載的, 而Sample變量類型聲名和轉型裏的Sample類倒是由類加載器(默認爲 AppClassLoader)加載的, 所以是徹底不一樣的類型, 因此會拋出轉型異常。
類加載器的代理/雙親委託原則, 決定了每個類在一個加載器裏最多加載一次, 固然多個加載器能夠加載同一個類。
每一個類對象在各自的namespace內,對類對象進行比較或者對實例進行類型轉換時,會同時比較各自的名字空間。
5.自定義類加載器
自定義加載器給Java語言增長了不少靈活性,主要的用途有
下面的代碼是上面示例中用到的自定義類加載器的實現類, 功能是從本地文件系統加載類
package classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; 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"; } }在該代碼中必需要說明的一點是, 該自定義類加載器的並無指定父加載器。
class CustomCL extends ClassLoader { private String basedir; // 須要該類加載器直接加載的類文件的基目錄 private HashSet dynaclazns; // 須要由該類加載器直接加載的類名 public CustomCL(String basedir, String[] clazns) { super(null); // 指定父類加載器爲 null this.basedir = basedir; dynaclazns = new HashSet(); loadClassByMe(clazns); } private void loadClassByMe(String[] clazns) { for (int i = 0; i < clazns.length; i++) { loadDirectly(clazns[i]); dynaclazns.add(clazns[i]); } } private Class loadDirectly(String name) { Class cls = null; StringBuffer sb = new StringBuffer(basedir); String classname = name.replace('.', File.separatorChar) + ".class"; sb.append(File.separator + classname); File classF = new File(sb.toString()); cls = instantiateClass(name,new FileInputStream(classF), classF.length()); return cls; } private Class instantiateClass(String name,InputStream fin,long len){ byte[] raw = new byte[(int) len]; fin.read(raw); fin.close(); return defineClass(name,raw,0,raw.length); } protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { Class cls = null; cls = findLoadedClass(name); if(!this.dynaclazns.contains(name) && cls == null) cls = getSystemClassLoader().loadClass(name); if (cls == null) throw new ClassNotFoundException(name); if (resolve) resolveClass(cls); return cls; } }在上面的自定義類加載器中, 咱們設置了該自定義類加載器的父加載器爲null, 那麼當咱們在使用自定義類加載器加載的類中引用第三方的類, 例如引用了原本應該是由擴展類加載器或者系統加載器加載的類時, 就會出現不能加載的問題。
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { Class cls = null; cls = findLoadedClass(name); if(!this.dynaclazns.contains(name) && cls == null) cls = getSystemClassLoader().loadClass(name); if (cls == null) throw new ClassNotFoundException(name); if (resolve) resolveClass(cls); return cls; }6. 類加載方式
6.1. 隱式加載
A a = new A();若是程序運行到這段代碼時尚未A類,那麼JVM會請求裝載當前類的類裝器來裝載類。
6.2. 顯示加載
//效果相同, 執行類的初始化 Class.forName("test.A"); Class.forName("test.A", true, this.getClass().getClassLoader()); //效果相同, 不執行類的初始化 getClass().getClassLoader().loadClass("test.A"); Class.forName("test.A", false, this.getClass().getClassLoader()); //效果相同, 不執行類的初始化 ClassLoader.getSystemClassLoader().loadClass("test.A"); Class.forName("test.A", false, Classloader.getSystemClassLoader()); //效果相同, 不執行類的初始化 Thread.currentThread().getContextClassLoader().loadClass("test.A") Class.forName("test.A", false, Thread.currentThread().getContextClassLoader());7. 上下文類加載器( ContextClassLoader)
在線程中運行的代碼能夠經過此類加載器來加載類和資源。
正常的雙親委派模型中,下層的類加載器可使用上層父加載器加載的對象,可是上層父類的加載器不可使用子類加載的對象。
而有些時候程序的確須要上層調用下層,這時候就須要線程上下文加載器來處理。
Thread.currentThread().getContextClassLoader()前面提到的類加載器的代理模式並不能解決 Java 應用開發中會遇到的類加載器的所有問題。
在給出代碼以前先說下Tomcat.6的類加載器, 結構層次以下:
+-----------------------------+ | Bootstrap | | | | | System | | | | | Common | | / \ | | WebApp1 WebApp2 | | | | | +-----------------------------+Webapp 類裝載器:
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { Class cls = null; cls = findLoadedClass(name); if(!this.dynaclazns.contains(name) && cls == null) cls = getSystemClassLoader().loadClass(name); if (cls == null) //自定義加載器和系統加載器均不能正常加載的類, 交由上下文加載器加載 cls = Thread.currentThread().getContextClassLoader().loadClass(name); if(cls == null) throw new ClassNotFoundException(name); if (resolve) resolveClass(cls); return cls; }其實ContextClassLoader就是Thread的一個屬性而已, 咱們固然能夠不使用ContextClassLoader, 本身找個地方把classLoader保存起來, 在須要獲取的時候能獲得此classLoader就能夠。
8.1. 熱加載
每次建立一個新的類加載器, 咱們修改下上面示例中的ClassIdentity類, 讓他能夠實現熱加載。
public class ClassIdentity extends Thread { public static void main(String[] args) { new ClassIdentity().start(); } public void run() { while(true) { this.testClassIdentity(); try { Thread.sleep(30 * 1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } public void testClassIdentity() { String classDataRootPath = "C:\\Documents and Settings\\Administrator\\workspace\\Classloader\\classData"; FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); String className = "com.example.Sample"; try { Class<?> class1 = fscl1.loadClass(className); Object obj1 = class1.newInstance(); Class<?> class2 = fscl2.loadClass(className); Object obj2 = class2.newInstance(); Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); setSampleMethod.invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } } }
運行該代碼, 在運行過程當中咱們修改Sample類, 並覆蓋原Sample類。
8.2. 類加密 指通常意義上的加密, 經過自定義加載器解密載入加密類 8.3. 應用隔離 很是典型的應用就是web容器