這是我們 java 虛擬機(jī)系列的第四篇文章, 類加載器
1.類加載器
Java 虛擬機(jī)的主要任務(wù)是裝載 class 文件并且執(zhí)行其中的字節(jié)碼邑彪。類加載器的作用是加載程序或 Java API 的 class 文件瞧毙,并將字節(jié)碼加載到執(zhí)行引擎。
在加載 class 文件時寄症, 為了防止加載進(jìn)來惡意的代碼宙彪,需要在類的加載器體系中去實(shí)現(xiàn)一些規(guī)則,保證在 Java 沙箱的安全模型有巧。
類加載其在 Java 沙箱中主要是三方面
- 守護(hù)了被信任的類庫的邊界 - 通過雙親委托機(jī)制實(shí)現(xiàn)
- 防止惡意代碼去干涉善意代碼 - 通過不同的命名空間去實(shí)現(xiàn)
- 將代碼歸入某類(稱為保護(hù)域,該類確定了代碼可以進(jìn)行哪些操作释漆。
這三方面我們后續(xù)會一個一個說
首先我們先看看在 Java 虛擬機(jī)中的整個類加載器體系
2. 雙親委托機(jī)制
類加載器體系守護(hù)了被信任的類庫的邊界,這是通過分別使用不同的類加載器加載可靠包和不可靠包來實(shí)現(xiàn)的篮迎。
這些不同的類加載器之間的依賴關(guān)系男图,構(gòu)成了 Java 虛擬機(jī)中的雙親委托機(jī)制。所謂的雙親委托機(jī)制甜橱,是指類加載器請求另一個類加載器來加載類的過程逊笆。
上圖是類加載器雙親委托模型,我們可以看到岂傲,除了啟動類加載器以外的每一個類加載器难裆,都有一個 ”雙親“ 類加載器,在某個特定的類加載器試圖以常用的方式加載類以前,它會默認(rèn)將這個任務(wù) ”委托“ 給它的雙親 -- 請求它的雙親來加載這個類乃戈。這個雙親再依次請求它自己的雙親來加載這個類褂痰。這個委托的過程一直向上繼續(xù),直到達(dá)到啟動類加載器偏化。如果一個類加載器的雙親類加器有能力來加載這個類脐恩,則這個類加載器返回這個類。否則侦讨,這個類加載器試圖自己來加載這個類。
它們有著不同的啟動路徑
類加載器 | 路徑 |
---|---|
Bootstrap ClassLoader 啟動類加載器 | Load JRE\lib\rt.jar 或者 -Xbootclasspath 選項(xiàng)指定的 Jar 包 |
Extension ClassLoader 擴(kuò)展類加載器 | Load JRE\lib\ext*.jar 或 -Djava.ext.dirs 指定目錄下的 Jar 包 |
Application ClassLoader 應(yīng)用程序類加載器 | Load CLASSPATH 或 -Djava.class.path 所指定的目錄下的類和 Jar 包 |
User ClassLoader 自定義類加載器 | 通過 Java.lang.ClassLoader 的子類自定義加載 class |
ClassLoader 的 loadClass 方法和 findClass 方法苟翻,如果是我們自定義 ClassLoader 的話韵卤,只需要重寫 findClass 方法即可
下面我們用一個例子來說明來加載器的雙親委托機(jī)制。我們自定義一個 ClassLoader 并復(fù)寫它的 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 文件版本放到桌面沈条,并且指定路徑進(jìn)行加載。
然后在 main 方法中運(yùn)行诅炉,將路徑設(shè)置為 我們在上面的桌面 視圖加載 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
, 設(shè)置路徑為我們的電腦桌面。這時候運(yùn)行涕烧,看看輸出
class name: Test1
class hashcode: 1265094477
loader: AppClassLoader
com.yxhuang.jvm.bytecode.Test1@7ea987ac
上面輸出月而,我們看看 Test1 類文件已經(jīng)加載進(jìn)了 類加載器,但是打印出來议纯,我們看到 ClassLoader 是 AppClassLoader 而不是我們自定義的 MyClassLoader父款。 為什么會這樣呢,這就涉及到類的雙親委托機(jī)制了瞻凤。
當(dāng)我們用 loader1 視圖去加載 com.yxhuang.jvm.bytecode.Test1
這個類的時候憨攒,根據(jù)雙親委托機(jī)制,自定義的類加載器 MyClassLoader 會委托它的父加載器 AppClassLoader 去加載阀参, AppClassLoader 應(yīng)用類加載器又會委托它的父類加載器 Bootstrap ClassLoader 啟動類去加載肝集。而 Bootstrap ClassLoader 找不到這個類,然后讓 AppClassLoader 去加載蛛壳,還記得上面提到 AppClassLoader 加載的路徑是項(xiàng)目的 ClassPath, 這時候找到了 Test1 類并加載了它杏瞻,并沒有讓 MyClassLoader 去加載
現(xiàn)在,我們把 out/production/class 路徑里面的 Test1 刪掉炕吸,再次運(yùn)行查看結(jié)果
這時候的輸出是
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 被調(diào)用了,加載 Test1 的路徑是 /Users/yxhuang/Desktop/com/
, 類加載器也是我們自定義的 MyClassLoader
MyClassLoader 會委托給它的父類赫模,最后到 啟動類加載器树肃,然后 MyClassLoader 之上的類加載器沒有一個能加載到,最后只能是 MyClassLoader 來加載瀑罗。
雙親委托機(jī)制可以保證父類先加載 Class 文件胸嘴,特別是 jdk 里面的類雏掠,保證 jdk 類的類優(yōu)先被啟動類加載器加載,防止惡意代碼偽裝成 jdk 的類去破壞 jvm 的運(yùn)行劣像。
雙親委托機(jī)制還有下面的一些特點(diǎn):
- 1.如果沒有顯示地傳遞一個雙親類裝載器給用戶自定義的類裝載器的構(gòu)造方法乡话,系統(tǒng)裝載器就默認(rèn)被指定為雙親。
- 2. 如果傳遞到構(gòu)造方法的是一個已有的用戶自定義類型裝載器的引用耳奕,該用戶自定義裝載器就被作為雙親绑青。
- 3.如果傳遞的方法是一個 null, 啟動類裝載器就是雙親。
- 4.在類裝載器之間具有了委派關(guān)系屋群,首先發(fā)起裝載要求的類裝載器不必是定義該類的類裝載器闸婴。
當(dāng)時雙親委托機(jī)制,也有它不足的地方芍躏,在不需要雙親委托機(jī)制的地方邪乍,需要上下文類加載器。關(guān)于上下文類加載器对竣,后面我們會講到庇楞,這里先跳過。
下面我們先看看命名空間
3. 類加器的命名空間
下面我們通過實(shí)例代碼否纬,說明命名空間
先定義一個 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 文件版本放到桌面谢床,并且指定路徑進(jìn)行加載许布。然后將 Persion 的 class 文件 刪除,同時注釋 Person汰规。
下面是命名空間的測試類, 設(shè)置加載路徑谬俄,用兩個不同的類加載器去加載 com.yxhuang.jvm.classloader.Person
類柏靶,然后通過反射,調(diào)用 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
根據(jù)上面的打印,我們可以知道钥勋, class1 和 class2 都是 Person 類炬转,但是 class1 == class2
是 false 的,說明他們不是同一個類算灸。
將 class1 和 class2 通過 newInstance() 方法生成對應(yīng)的 object1 和 object2 對象扼劈,這也都是 Person 類的對象。
在調(diào)用反射將 object1 的 setPerson 方法會拋出異常
public void setPerson(Object object){
System.out.println("setPerson " + object.getClass().getSimpleName());
this.person = (Person) object;
}
拋出的異常是說 Person 對象不能強(qiáng)轉(zhuǎn)成 Person 對象菲驴。這個異常就很奇怪了荐吵,那為什么會出現(xiàn)這個異常,那就要說到 java 虛擬機(jī)里面的命名空間了。
因?yàn)檫@兩個對象加載的虛擬機(jī)不一樣先煎,導(dǎo)致命名空間不一樣導(dǎo)致的贼涩。
命名空間是表示當(dāng)前類的加載器的命名空間,是由當(dāng)前類轉(zhuǎn)加載器是自己的初始類加載器的類型名稱組成的薯蝎。
命名空間的作用是通過不同的命名空間遥倦,防止惡意代碼去干涉其他代碼。在 Java 虛擬機(jī)中占锯,在同一個命名空間內(nèi)的類可以之間進(jìn)行交互袒哥,而不同的命名空間中的類察覺不到彼此的存在。
每個類裝載器都有自己的命名空間烟央,其中維護(hù)者由它裝載的類型统诺。所以一個 Java 程序可以多次裝載具有一個全限定名的多個類型。這樣一個類的全限定名就不足以確定在一個 Java 虛擬機(jī)中的唯一性疑俭。因此,當(dāng)多個類裝載器都裝載了同名的類型時婿失,為了唯一地標(biāo)識該類型钞艇,還要在類型名稱前加上裝載器該類(指出了它所位于的命名空間)的類裝載器的標(biāo)識。
上面 Person 的這個例子就說明豪硅,一個類的全限定名 com.yxhuang.jvm.classloader.Person
不能確定它的唯一性哩照,我們可以用另外一個類加載器去再次加載這個類。
綜上所述懒浮,如果想要確定一個類是否是唯一的或者說判斷兩個類是否相等飘弧,就需要他們的類加載器為同一個累加器,并且命名空間是一致的砚著。
關(guān)于命名空間的一些論述
- 每個類裝載器都有自己的命名空間次伶,命名空間由該裝載器及其父裝載器所裝載的類組成;
- 在同一個命名空間中稽穆,不會出現(xiàn)類的完整姓名(包括類的包名)相同的兩個類冠王;
- 在不同的命名空間中,有可能會出現(xiàn)類的完整名字(包含類的包名)相同的兩個類舌镶。
類裝載器和這個類本身一起共同確立在 Java 虛擬機(jī)中的唯一性柱彻,每一個類裝載器,都有一個獨(dú)立的命名空間餐胀。
也就是說哟楷,比較兩個類是否”相等“,只有這兩個類是由同一個類裝載器的前提下否灾,否則卖擅,即使這兩個類來源于同一個 Class 文件,被同一個 Java 虛擬機(jī)加載,只要加載它們的類裝載器不同磨镶,那這兩個類就必定不相等溃蔫。
不同的加載器實(shí)例加載的類被認(rèn)為是不同的類
在 JVM 的實(shí)現(xiàn)中有一條隱含的規(guī)則,默認(rèn)情況下琳猫,如果一個類由類加載器 A 加載伟叛,那么這個類的依賴類也是由相同的類加載器加載
上面的幾條論述在例子中也有體現(xiàn)。
4 自定義類加載器
4.1 自定義類加載器
如果想要自定義類加載器脐嫂,只需要繼承 ClassLoader 并且重寫它的 findClass() 方法统刮。
在 findClass() 方法里面根據(jù)路徑去加載相應(yīng)的 Class 文件流,然后將數(shù)據(jù)傳遞給 ClassLoader 自帶的 defineClass() 方法账千,defineClass() 會將Class 流文件轉(zhuǎn)成 Class 類的實(shí)例侥蒙。
@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 將流文件轉(zhuǎn)成一個 Class 類實(shí)例
return defineClass(className, classData, 0, classData.length);
}
除此之外,必須要了解 ClassLoader 里面的 loadClass() 方法
4.2 loadClass() 方法
在我們自定義了 ClassLoader 之后匀奏,會調(diào)用 loadClass() 方法去加載想要加載的類鞭衩。
loadClass() 的基本工作方式:
給定需要查找的類型的全限定名, loadClass()方法會用某種方式找到或生成字節(jié)數(shù)組到娃善,里面的數(shù)據(jù)采用 Java Class 文件格式(用該格式定義類型)论衍。如果 loadClass() 無法找到或生成這些字節(jié),就會拋出 ClassNotFoundException 異常聚磺。否則坯台,loadClass() 會傳遞這個自己數(shù)組到 ClassLoader 聲明的某一個 defineClass() 方法。通過把這些字節(jié)數(shù)組傳遞給
defineClass(),loadClass() 會要求虛擬機(jī)把傳入的字節(jié)數(shù)組導(dǎo)入這個用戶自定義的類裝載器的命名中間中去瘫寝。-
loadClass 的步驟:
- 1.查看是否請求的類型已經(jīng)被這個類裝載器裝載進(jìn)命名空間(提供 findLoadedClass())方法的工作方式
- 2.否則蜒蕾,委派到這個類裝載器的雙親裝載器。如果雙親返回了一個 Class 實(shí)例焕阿,就把這個 Class 實(shí)例返回咪啡。
- 否則,調(diào)用 findClass(), findClass() 會試圖尋找或者生成一個字節(jié)數(shù)組捣鲸,內(nèi)容采用 Java Class 文件格式(它定義了所需要的類型)瑟匆。如果成功,findClass() 把這個字節(jié)傳遞給 defineClass() 栽惶,后者試圖導(dǎo)入這個類型愁溜,返回一個 Class 實(shí)例。 如果 findClass() 返回一個 Class 實(shí)例外厂,loadClass() 就會把這個實(shí)例返回冕象。
- 否則, findClass() 拋出某些異常來中止處理汁蝶,而且 loadClass() 也會拋出異常中止渐扮。
public abstract class ClassLoader {
//每個類加載器都有個父加載器
private final ClassLoader parent;
public Class<?> loadClass(String name) {
//查找一下這個類是不是已經(jīng)加載過了
Class<?> c = findLoadedClass(name);
//如果沒有加載過
if( c == null ){
//先委托給父加載器去加載论悴,注意這是個遞歸調(diào)用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加載器為空,查找Bootstrap加載器是不是加載過了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加載器沒加載成功墓律,調(diào)用自己的findClass去加載
if (c == null) {
c = findClass(name);
}
return c膀估;
}
protected Class<?> findClass(String name){
//1. 根據(jù)傳入的類名name,到在特定目錄下去尋找類文件耻讽,把.class文件讀入內(nèi)存
...
//2. 調(diào)用defineClass將字節(jié)數(shù)組轉(zhuǎn)成Class對象
return defineClass(buf, off, len)察纯;
}
// 將字節(jié)碼數(shù)組解析成一個Class對象,用native方法實(shí)現(xiàn)
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
5 線程上下文類加載器
雙親委托機(jī)制不適用的場景下针肥,需要使用到 上下文類加載器(Thread Context ClassLoader)
場景是有基礎(chǔ)類要調(diào)用用戶代碼(Service Provider Interface, SPI)
線程上下文加載器通過 Thread 類的 setContextClassLoader() 方法進(jìn)行設(shè)置饼记,如果創(chuàng)建線程還未設(shè)置,就會從父線程中繼承一個慰枕,如果在應(yīng)用程序的全局范圍都沒有設(shè)置過的話具则,那這個類裝載器默認(rèn)是應(yīng)用類加載器。
6.獲取 ClassLoader 的途徑
獲取當(dāng)前類的 ClassLoader: clazz.getClassLoader()
獲取當(dāng)前線程上下文的 ClassLoader: Thread.currentThread().getContextClassLoader()
獲取系統(tǒng)的 ClassLoader : ClassLoader.getSystemClassLoader()
獲取調(diào)用者的 ClassLoader: DriverManager.getCallerClassLoader()