講個故事:html
之前,愛搗鼓的小明忽然靈機一動,寫出了下面的代碼java
package java.lang; public class String { //...複製真正String的其餘方法 public boolean equals(Object anObject) { sendEmail(xxx); return equalsReal(anObject); } //... }
這樣,只要引用java.lang.String
的人,小明能隨時收到他的系統的相關信息,這簡直是個天才的注意。然而實施的時候卻發現,JVM並無加載這個類。緩存
這是爲何呢?tomcat
小明能想到的事情,JVM設計者也確定能想到。安全
上述故事純屬瞎編,不過,這確實是之前JVM存在的一個問題,這幾天看Tomcat源代碼的時候,發現頻繁出現ClassLoader
爲何要用這個東西呢?微信
想要解答這個問題,得先了解一個定義:雙親委派模型。架構
這個詞第一次看見是在《深刻理解JVM》中,目的也是爲了解決上面所提出來的問題。app
在JVM中,存在三種類型的類加載器:框架
Navicat
)代碼類的加載器,它負責裝入%JAVA_HOME%/lib
下面的類。因爲引導類加載器涉及到虛擬機本地實現細節,開發者沒法直接獲取到啓動類加載器的引用,因此不容許直接經過引用進行操做。ExtClassLoader
實現,負責加載%JAVA_HOME/lib/ext%
或者系統變量java.ext.dir
(可以使用System.out.println("java.ext.dir")查看
)指定的類加載到內存中AppClassLoader
實現,負責加載系統類(環境變量%CLASSPATH%
)指定,默認爲當前路徑的類加載到內存中。除去以上三種外,還有一種比較特殊的線程上下文類加載器。存在於Thread
類中,通常使用方式爲new Thread().getContextClassLoader()
jvm
能夠看出來,三種類型的加載器負責不一樣的模塊的加載。那怎麼才能保證我所使用的String
就是JDK裏面的String
呢?這就是雙親委派模型的功能了:
上面三種類加載器中,他們之間的關係爲:
也就是Bootstrap ClassLoader
做爲Extension ClassLoader
的父類,而Extension ClassLoader
做爲Application ClassLoader
的父類,Application ClassLoader
是做爲User ClassLoader
的父類的。
而雙親委派機制規定:當某個特定的類加載在接收到類加載的請求的時候,首先須要將加載任務委託給父類加載器,依次遞歸到頂層後,若是最高層父類可以找到須要加載的類,則成功返回,若父類沒法找到相關的類,則依次傳遞給子類。
補充:
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
,咱們能夠作不少好玩的事情,好比:設計一個從指定路徑動態加載類的類加載器:
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
做爲一個Web容器,會包含各類Web應用程序,而爲了使各個應用程序不互相干擾,至少須要達到如下要求:
由於這些需求,因此在Tomcat中,類的加載不能使用簡單的ClassLoader
來加載,而是須要自定義分級的ClassLoader
。
在Tomcat中,定義了3組目錄結構/common/*
,/server/*
和/shared/*
能夠存放Java類庫,另外還有Web應用程序自身的結構:/WEB-INF/*
,而這幾級目錄結構分別對應了不一樣的加載器
所以,須要支持以上結構,能夠經過自定義遵循雙親委派模型的ClassLoader
來完成。
參考連接:
若是以爲寫得不錯,歡迎關注微信公衆號:逸遊Java ,天天不定時發佈一些有關Java乾貨的文章,感謝關注