講個故事:
以前,愛搗鼓的小明突然靈機一動,寫出了下面的代碼
package java.lang;
public class String {
//...復制真正String的其他方法
public boolean equals(Object anObject) {
sendEmail(xxx);
return equalsReal(anObject);
}
//...
}
這樣,只要引用java.lang.String
的人,小明能隨時收到他的系統的相關信息,這簡直是個天才的注意。然而實施的時候卻發現,JVM並沒有加載這個類。
這是為什么呢?
小明能想到的事情,JVM設計者也肯定能想到。
雙親委派模型
上述故事純屬瞎編,不過,這確實是以前JVM存在的一個問題,這幾天看Tomcat源代碼的時候,發現頻繁出現ClassLoader
為什么要用這個東西呢?
想要解答這個問題,得先了解一個定義:雙親委派模型。
這個詞第一次看見是在《深入理解JVM》中,目的也是為了解決上面所提出來的問題。
在JVM中,存在三種類型的類加載器:
- 啟動類(Bootstrap)加載器: 用於加載本地(
Navicat
)代碼類的加載器,它負責裝入%JAVA_HOME%/lib
下面的類。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。 - 標准擴展(Extension)類加載器: 由
ExtClassLoader
實現,負責加載%JAVA_HOME/lib/ext%
或者系統變量java.ext.dir
(可使用System.out.println("java.ext.dir")查看
)指定的類加載到內存中 - 系統(System)類加載器: 由
AppClassLoader
實現,負責加載系統類(環境變量%CLASSPATH%
)指定,默認為當前路徑的類加載到內存中。
除去以上三種外,還有一種比較特殊的線程上下文類加載器。存在於Thread
類中,一般使用方式為new Thread().getContextClassLoader()
可以看出來,三種類型的加載器負責不同的模塊的加載。那怎么才能保證我所使用的String
就是JDK里面的String
呢?這就是雙親委派模型的功能了:
上面三種類加載器中,他們之間的關系為:
也就是Bootstrap ClassLoader
作為Extension ClassLoader
的父類,而Extension ClassLoader
作為Application ClassLoader
的父類,Application ClassLoader
是作為User ClassLoader
的父類的。
而雙親委派機制規定:當某個特定的類加載在接收到類加載的請求的時候,首先需要將加載任務委托給父類加載器,依次遞歸到頂層后,如果最高層父類能夠找到需要加載的類,則成功返回,若父類無法找到相關的類,則依次傳遞給子類。
補充:
- 如果A類引用了B,則JVM將使用加載類A的加載器加載類B
- 類加載器存在緩存,如果某個加載器以前成功加載過某個類后,再次接受到此類加載請求則直接返回,不再向上傳遞加載請求
- 可以通過
ClassLoader.loadClass()
或Class.ForName(xxx,true,classLoader)
指定某個加載器加載類 - 類類型由加載它的加載器和這個類本身共同決定,如果類加載器不同,類名相同,
instanceof
依然會返回false
- 父加載器無法加載子加載器能夠加載的類
可以看到,通過雙親委派機制,能夠保證使用的類的安全性,並且可以避免類重名的情況下JVM存在多個相同的類名相同,字節碼不同的類。
回到剛開始講的故事,雖然小明自定義了
String
,包名也叫java.lang
,但是當用戶使用String
的時候,會由普通的Application ClassLoader
加載java.lang.String
,此時通過雙親委派,類加載請求會上傳給Application ClassLoader
的父類,直到傳遞給Bootstrap ClassLoader
,而此時,Bootstrap ClassLoader
將在%JAVA_HOME%/lib中尋找java.lang.String
而此時正好能夠找到java.lang.String
,加載成功,返回。因此小明自己寫的java.lang.String
並沒有被加載。
可以看見,如果真的想要實現小明的計划,只能將小明自己編寫的
java.lang.String
這個class
文件替換到%JAVA_HOME%/lib/rt.jar 中的String.class
自定義ClassLoader
到這里,估計能明白為什么需要雙親委派模型了,而某些時候,我們可以看見許多框架都自定義了ClassLoader
,通過自定義ClassLoader
,我們可以做很多好玩的事情,比如:設計一個從指定路徑動態加載類的類加載器:
public class DiskClassLoader extends ClassLoader {
private String libPath;
public DiskClassLoader(String path){
libPath=path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try(FileInputStream fileInputStream=new FileInputStream(new File(libPath,getFileName(name)));
BufferedInputStream bufferedInputStream=new BufferedInputStream(fileInputStream);
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream()){
for (int len=0;(len=bufferedInputStream.read())!=-1;){
byteArrayOutputStream.write(len);
}
byte[] data=byteArrayOutputStream.toByteArray();
return defineClass(name,data,0,data.length);
}catch (IOException e){
e.printStackTrace();
}
return super.findClass(name);
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
上面是一個簡單的例子,可以看見想要自定義ClassLoader
,只需要繼承ClassLoader
,然后覆蓋findClass()
方法即可,其中findClass()
是負責獲取指定類的字節碼的,在獲取到字節碼后,需要手動調用defineClass()
加載類。
在ClassLoader
類中,我們能找到loadClass
的源代碼:
protected Class<?> loadClass(String name, boolean resolve) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
在刪減掉一些模板代碼后,我們可以看到loadClass()
方法就是實現雙親委派的主要代碼:首先查看類是否有緩存,如果沒有,就調用父類的loadClass
方法,讓父類去加載,如果父類加載失敗,則自己加載,如果自己加載失敗,那就返回null
,注意:並沒有再找自己的子類去尋找類,也就是在哪里發起的加載,就在哪里結束。
這里可以看到,
loadClass()
方法並沒有被標記為final
的,也就是我們依然可以重載它的loadClass()
方法,破壞原本的委派雙親模型。
破壞雙親委派機制
有些時候,雙親委派機制也會遇到一些問題,在介紹雙親委派機制的時候,我列舉了一些補充。而在一些JDK中,存在一些基礎API
他們的加載由比較上層的加載器負責,這些API
只是一些簡單的接口,而具體的實現可能會由其他用戶自己實現,這個時候就存在一個問題,如果這些基礎的API
需要調用/加載用戶的代碼的時候,會發現由於父類無法找到子類所能加載的類的原因,調用失敗。
最典型的例子便是JNDI
服務,JNDI
服務是在JDK1.3
的時候放入rt.jar
中,而rt.jar
有Bootstrap ClassLoader
加載,JNDI
的功能是對資源進行集中管理和查找,它需要調用獨立廠商實現部部署在應用程序的classpath
下的JNDI
接口提供者(SPI, Service Provider Interface)
的代碼,但啟動類加載器不可能“認識”之些代碼,該怎么辦?
這就需要用到最開始講的特殊的加載器:上下文類加載器
上下文類加載器的使用方式為:Thread.currentThread().getContextClassLoader()
上下文類加載器是什么意思呢?可以看源碼,Thread
初始化是通過本地方法currentThread();
初始化的,而classLoader
也正是通過currentThread
初始化,currentThread
指的是當前正在運行的線程。
而默認情況下,啟動Launcher
后,Launcher
會將當前線程的上下文加載器設置為Application ClassLoader
public Launcher() {
//...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
//...
}
因此,上下文類加載器默認就是系統加載器,通過上下文加載器,更高級別的加載器便可以調用系統加載器加載一個類。
Tomcat 與類加載器
Tomcat
作為一個Web容器,會包含各種Web應用程序,而為了使各個應用程序不互相干擾,至少需要達到以下要求:
- 部署在同一個Web容器上的兩個Web應用程序所使用的Java類庫可以實現相互隔離
- 部署在同一個Web容器上的兩個Web應用程序所使用的Java類庫可以相互共享
- Web容器需要保證自身的安全不受Web應用程序所影響
- 只是JSP的容器,需要支持熱部署功能
因為這些需求,所以在Tomcat中,類的加載不能使用簡單的ClassLoader
來加載,而是需要自定義分級的ClassLoader
。
在Tomcat中,定義了3組目錄結構/common/*
,/server/*
和/shared/*
可以存放Java類庫,另外還有Web應用程序自身的結構:/WEB-INF/*
,而這幾級目錄結構分別對應了不同的加載器
- common: 類庫可以被Tomcat和所有Web應用程序共同使用
- server: 類庫可以被Tomcat使用,對其他Web程序不可見
- shared: 類庫可以被所有的Web應用程序共同使用,但對Tomcat不可見
- **WEB-INF: ** 類庫僅僅能被自身Web應用程序使用
因此,需要支持以上結構,可以通過自定義遵循雙親委派模型的ClassLoader
來完成。
參考鏈接:
如果覺得寫得不錯,歡迎關注微信公眾號:逸游Java ,每天不定時發布一些有關Java干貨的文章,感謝關注