這是我們 java 虛擬機系列的第四篇文章, 類加載器
1.類加載器
Java 虛擬機的主要任務是裝載 class 文件并且執行其中的字節碼。類加載器的作用是加載程序或 Java API 的 class 文件,并將字節碼加載到執行引擎。
在加載 class 文件時, 為了防止加載進來惡意的代碼,需要在類的加載器體系中去實現一些規則,保證在 Java 沙箱的安全模型。
類加載其在 Java 沙箱中主要是三方面
- 守護了被信任的類庫的邊界 - 通過雙親委托機制實現
- 防止惡意代碼去干涉善意代碼 - 通過不同的命名空間去實現
- 將代碼歸入某類(稱為保護域,該類確定了代碼可以進行哪些操作。
這三方面我們后續會一個一個說
首先我們先看看在 Java 虛擬機中的整個類加載器體系
2. 雙親委托機制
類加載器體系守護了被信任的類庫的邊界,這是通過分別使用不同的類加載器加載可靠包和不可靠包來實現的。
這些不同的類加載器之間的依賴關系,構成了 Java 虛擬機中的雙親委托機制。所謂的雙親委托機制,是指類加載器請求另一個類加載器來加載類的過程。
上圖是類加載器雙親委托模型,我們可以看到,除了啟動類加載器以外的每一個類加載器,都有一個 ”雙親“ 類加載器,在某個特定的類加載器試圖以常用的方式加載類以前,它會默認將這個任務 ”委托“ 給它的雙親 -- 請求它的雙親來加載這個類。這個雙親再依次請求它自己的雙親來加載這個類。這個委托的過程一直向上繼續,直到達到啟動類加載器。如果一個類加載器的雙親類加器有能力來加載這個類,則這個類加載器返回這個類。否則,這個類加載器試圖自己來加載這個類。
它們有著不同的啟動路徑
類加載器 | 路徑 |
---|---|
Bootstrap ClassLoader 啟動類加載器 | Load JRE\lib\rt.jar 或者 -Xbootclasspath 選項指定的 Jar 包 |
Extension ClassLoader 擴展類加載器 | Load JRE\lib\ext*.jar 或 -Djava.ext.dirs 指定目錄下的 Jar 包 |
Application ClassLoader 應用程序類加載器 | Load CLASSPATH 或 -Djava.class.path 所指定的目錄下的類和 Jar 包 |
User ClassLoader 自定義類加載器 | 通過 Java.lang.ClassLoader 的子類自定義加載 class |
ClassLoader 的 loadClass 方法和 findClass 方法,如果是我們自定義 ClassLoader 的話,只需要重寫 findClass 方法即可
下面我們用一個例子來說明來加載器的雙親委托機制。我們自定義一個 ClassLoader 并復寫它的 findClass() 方法
@Override
protected Class findClass(String className) throws ClassNotFoundException {
System.out.println("findClass className: " + className);
byte[] classData;
classData = getTypeFromBasePath(className);
if (classData == null){
throw new ClassNotFoundException();
}
// Parse it
return defineClass(className, classData, 0, classData.length);
}
private byte[] getTypeFromBasePath(String typeName){
FileInputStream fis;
String fileName = path + typeName.replace('.', File.separatorChar) + ".class";
System.out.println("getTypeFromBasePath fileName :" + fileName);
try {
fis = new FileInputStream(fileName);
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
BufferedInputStream bis = new BufferedInputStream(fis);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
int c = bis.read();
while ( c != -1){
out.write(c);
c = bis.read();
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
return out.toByteArray();
}
然后在我們通過 IDEA 編譯,將編譯出來的 Class 文件版本放到桌面,并且指定路徑進行加載。
然后在 main 方法中運行,將路徑設置為 我們在上面的桌面 視圖加載 Test1 類
public static void main(String[] args) throws Exception {
// loadClass
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("/Users/yxhuang/Desktop/");
Class<?> clazz = loader1.loadClass("com.yxhuang.jvm.bytecode.Test1");
System.out.println("class name: " + clazz.getSimpleName() + " \nclass hashcode: " + clazz.hashCode() + " \nloader: " + clazz.getClassLoader().getClass().getSimpleName());
Object object1 = clazz.newInstance();
System.out.println(object1);
}
上面的例子,我們將 MyClassLoader
命名為 loader1
, 設置路徑為我們的電腦桌面。這時候運行,看看輸出
class name: Test1
class hashcode: 1265094477
loader: AppClassLoader
com.yxhuang.jvm.bytecode.Test1@7ea987ac
上面輸出,我們看看 Test1 類文件已經加載進了 類加載器,但是打印出來,我們看到 ClassLoader 是 AppClassLoader 而不是我們自定義的 MyClassLoader。 為什么會這樣呢,這就涉及到類的雙親委托機制了。
當我們用 loader1 視圖去加載 com.yxhuang.jvm.bytecode.Test1
這個類的時候,根據雙親委托機制,自定義的類加載器 MyClassLoader 會委托它的父加載器 AppClassLoader 去加載, AppClassLoader 應用類加載器又會委托它的父類加載器 Bootstrap ClassLoader 啟動類去加載。而 Bootstrap ClassLoader 找不到這個類,然后讓 AppClassLoader 去加載,還記得上面提到 AppClassLoader 加載的路徑是項目的 ClassPath, 這時候找到了 Test1 類并加載了它,并沒有讓 MyClassLoader 去加載
現在,我們把 out/production/class 路徑里面的 Test1 刪掉,再次運行查看結果
這時候的輸出是
findClass className: com.yxhuang.jvm.bytecode.Test1
getTypeFromBasePath fileName :/Users/yxhuang/Desktop/com/yxhuang/jvm/bytecode/Test1.class
class name: Test1
class hashcode: 1554874502
loader: MyClassLoader
com.yxhuang.jvm.bytecode.Test1@6e0be858
看到上面的輸出,我們看到我們自定義的 MyClassLoader 被調用了,加載 Test1 的路徑是 /Users/yxhuang/Desktop/com/
, 類加載器也是我們自定義的 MyClassLoader
MyClassLoader 會委托給它的父類,最后到 啟動類加載器,然后 MyClassLoader 之上的類加載器沒有一個能加載到,最后只能是 MyClassLoader 來加載。
雙親委托機制可以保證父類先加載 Class 文件,特別是 jdk 里面的類,保證 jdk 類的類優先被啟動類加載器加載,防止惡意代碼偽裝成 jdk 的類去破壞 jvm 的運行。
雙親委托機制還有下面的一些特點:
- 1.如果沒有顯示地傳遞一個雙親類裝載器給用戶自定義的類裝載器的構造方法,系統裝載器就默認被指定為雙親。
- 2. 如果傳遞到構造方法的是一個已有的用戶自定義類型裝載器的引用,該用戶自定義裝載器就被作為雙親。
- 3.如果傳遞的方法是一個 null, 啟動類裝載器就是雙親。
- 4.在類裝載器之間具有了委派關系,首先發起裝載要求的類裝載器不必是定義該類的類裝載器。
當時雙親委托機制,也有它不足的地方,在不需要雙親委托機制的地方,需要上下文類加載器。關于上下文類加載器,后面我們會講到,這里先跳過。
下面我們先看看命名空間
3. 類加器的命名空間
下面我們通過實例代碼,說明命名空間
先定義一個 Person 類
public class Person {
private Person person;
public Person() {
}
public void setPerson(Object object){
System.out.println("setPerson " + object.getClass().getSimpleName());
this.person = (Person) object;
}
}
用 IDEA 編譯成 class 文件,將編譯出來的 Class 文件版本放到桌面,并且指定路徑進行加載。然后將 Persion 的 class 文件 刪除,同時注釋 Person。
下面是命名空間的測試類, 設置加載路徑,用兩個不同的類加載器去加載 com.yxhuang.jvm.classloader.Person
類,然后通過反射,調用 class1 的 setPerson
方法。
public class NameSpaceLoaderTest {
public static void main(String[] arg) throws Exception {
MyClassLoader classLoader1 = new MyClassLoader("classloader1");
MyClassLoader classLoader2 = new MyClassLoader("classloader2");
classLoader1.setPath("/Users/yxhuang/Desktop/");
classLoader2.setPath("/Users/yxhuang/Desktop/");
Class<?> class1 = classLoader1.loadClass("com.yxhuang.jvm.classloader.Person");
Class<?> class2 = classLoader2.loadClass("com.yxhuang.jvm.classloader.Person");
System.out.println("class1 : " + class1.getSimpleName() + " " + class1.getClassLoader().toString());
System.out.println("class2 : " + class2.getSimpleName() + " " + class2.getClassLoader().toString());
System.out.println(class1 == class2);
Object object1 = class1.newInstance();
Object object2 = class2.newInstance();
Method method = class1.getMethod("setPerson", Object.class);
method.invoke(object1, object2);
}
}
然后,我們看看輸出
findClass className: com.yxhuang.jvm.classloader.Person
getTypeFromBasePath fileName :/Users/yxhuang/Desktop/com/yxhuang/jvm/classloader/Person.class
findClass className: com.yxhuang.jvm.classloader.Person
getTypeFromBasePath fileName :/Users/yxhuang/Desktop/com/yxhuang/jvm/classloader/Person.class
class1 : Person com.yxhuang.jvm.classloader.MyClassLoader@42a57993
class2 : Person com.yxhuang.jvm.classloader.MyClassLoader@6bc7c054
false
// 還會拋出異常
Caused by: java.lang.ClassCastException: com.yxhuang.jvm.classloader.Person cannot be cast to com.yxhuang.jvm.classloader.Person
根據上面的打印,我們可以知道, class1 和 class2 都是 Person 類,但是 class1 == class2
是 false 的,說明他們不是同一個類。
將 class1 和 class2 通過 newInstance() 方法生成對應的 object1 和 object2 對象,這也都是 Person 類的對象。
在調用反射將 object1 的 setPerson 方法會拋出異常
public void setPerson(Object object){
System.out.println("setPerson " + object.getClass().getSimpleName());
this.person = (Person) object;
}
拋出的異常是說 Person 對象不能強轉成 Person 對象。這個異常就很奇怪了,那為什么會出現這個異常,那就要說到 java 虛擬機里面的命名空間了。
因為這兩個對象加載的虛擬機不一樣,導致命名空間不一樣導致的。
命名空間是表示當前類的加載器的命名空間,是由當前類轉加載器是自己的初始類加載器的類型名稱組成的。
命名空間的作用是通過不同的命名空間,防止惡意代碼去干涉其他代碼。在 Java 虛擬機中,在同一個命名空間內的類可以之間進行交互,而不同的命名空間中的類察覺不到彼此的存在。
每個類裝載器都有自己的命名空間,其中維護者由它裝載的類型。所以一個 Java 程序可以多次裝載具有一個全限定名的多個類型。這樣一個類的全限定名就不足以確定在一個 Java 虛擬機中的唯一性。因此,當多個類裝載器都裝載了同名的類型時,為了唯一地標識該類型,還要在類型名稱前加上裝載器該類(指出了它所位于的命名空間)的類裝載器的標識。
上面 Person 的這個例子就說明,一個類的全限定名 com.yxhuang.jvm.classloader.Person
不能確定它的唯一性,我們可以用另外一個類加載器去再次加載這個類。
綜上所述,如果想要確定一個類是否是唯一的或者說判斷兩個類是否相等,就需要他們的類加載器為同一個累加器,并且命名空間是一致的。
關于命名空間的一些論述
- 每個類裝載器都有自己的命名空間,命名空間由該裝載器及其父裝載器所裝載的類組成;
- 在同一個命名空間中,不會出現類的完整姓名(包括類的包名)相同的兩個類;
- 在不同的命名空間中,有可能會出現類的完整名字(包含類的包名)相同的兩個類。
類裝載器和這個類本身一起共同確立在 Java 虛擬機中的唯一性,每一個類裝載器,都有一個獨立的命名空間。
也就是說,比較兩個類是否”相等“,只有這兩個類是由同一個類裝載器的前提下,否則,即使這兩個類來源于同一個 Class 文件,被同一個 Java 虛擬機加載,只要加載它們的類裝載器不同,那這兩個類就必定不相等。
不同的加載器實例加載的類被認為是不同的類
在 JVM 的實現中有一條隱含的規則,默認情況下,如果一個類由類加載器 A 加載,那么這個類的依賴類也是由相同的類加載器加載
上面的幾條論述在例子中也有體現。
4 自定義類加載器
4.1 自定義類加載器
如果想要自定義類加載器,只需要繼承 ClassLoader 并且重寫它的 findClass() 方法。
在 findClass() 方法里面根據路徑去加載相應的 Class 文件流,然后將數據傳遞給 ClassLoader 自帶的 defineClass() 方法,defineClass() 會將Class 流文件轉成 Class 類的實例。
@Override
protected Class findClass(String className) throws ClassNotFoundException {
System.out.println("findClass className: " + className);
byte[] classData;
// 指定路徑加載 Class 流文件
classData = getTypeFromBasePath(className);
if (classData == null){
throw new ClassNotFoundException();
}
// Parse it 將流文件轉成一個 Class 類實例
return defineClass(className, classData, 0, classData.length);
}
除此之外,必須要了解 ClassLoader 里面的 loadClass() 方法
4.2 loadClass() 方法
在我們自定義了 ClassLoader 之后,會調用 loadClass() 方法去加載想要加載的類。
loadClass() 的基本工作方式:
給定需要查找的類型的全限定名, loadClass()方法會用某種方式找到或生成字節數組到,里面的數據采用 Java Class 文件格式(用該格式定義類型)。如果 loadClass() 無法找到或生成這些字節,就會拋出 ClassNotFoundException 異常。否則,loadClass() 會傳遞這個自己數組到 ClassLoader 聲明的某一個 defineClass() 方法。通過把這些字節數組傳遞給
defineClass(),loadClass() 會要求虛擬機把傳入的字節數組導入這個用戶自定義的類裝載器的命名中間中去。-
loadClass 的步驟:
- 1.查看是否請求的類型已經被這個類裝載器裝載進命名空間(提供 findLoadedClass())方法的工作方式
- 2.否則,委派到這個類裝載器的雙親裝載器。如果雙親返回了一個 Class 實例,就把這個 Class 實例返回。
- 否則,調用 findClass(), findClass() 會試圖尋找或者生成一個字節數組,內容采用 Java Class 文件格式(它定義了所需要的類型)。如果成功,findClass() 把這個字節傳遞給 defineClass() ,后者試圖導入這個類型,返回一個 Class 實例。 如果 findClass() 返回一個 Class 實例,loadClass() 就會把這個實例返回。
- 否則, findClass() 拋出某些異常來中止處理,而且 loadClass() 也會拋出異常中止。
public abstract class ClassLoader {
//每個類加載器都有個父加載器
private final ClassLoader parent;
public Class<?> loadClass(String name) {
//查找一下這個類是不是已經加載過了
Class<?> c = findLoadedClass(name);
//如果沒有加載過
if( c == null ){
//先委托給父加載器去加載,注意這是個遞歸調用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加載器為空,查找Bootstrap加載器是不是加載過了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加載器沒加載成功,調用自己的findClass去加載
if (c == null) {
c = findClass(name);
}
return c;
}
protected Class<?> findClass(String name){
//1. 根據傳入的類名name,到在特定目錄下去尋找類文件,把.class文件讀入內存
...
//2. 調用defineClass將字節數組轉成Class對象
return defineClass(buf, off, len);
}
// 將字節碼數組解析成一個Class對象,用native方法實現
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
5 線程上下文類加載器
雙親委托機制不適用的場景下,需要使用到 上下文類加載器(Thread Context ClassLoader)
場景是有基礎類要調用用戶代碼(Service Provider Interface, SPI)
線程上下文加載器通過 Thread 類的 setContextClassLoader() 方法進行設置,如果創建線程還未設置,就會從父線程中繼承一個,如果在應用程序的全局范圍都沒有設置過的話,那這個類裝載器默認是應用類加載器。
6.獲取 ClassLoader 的途徑
獲取當前類的 ClassLoader: clazz.getClassLoader()
獲取當前線程上下文的 ClassLoader: Thread.currentThread().getContextClassLoader()
獲取系統的 ClassLoader : ClassLoader.getSystemClassLoader()
獲取調用者的 ClassLoader: DriverManager.getCallerClassLoader()