深入分析ClassLoader
why?
ClassLoader,即java類加載器,主要作用是將class加載到JVM內,同時它還要考慮class由誰來加載。在說java的類加載機制之前,還是像前面的博客一樣,先說說為什么要知道java的類加載機制。個人認為主要有以下幾個原因:
- 按需加載。JVM啟動時不能確定我要加載哪些東西,或者有些類非常大,我只希望用到它時再加載,并非一次性加載所有的class,所以這時候了解了加載機制就可以按需加載了。
- 類隔離。比如web容器中部署多個應用,應用之間互相可能會有沖突,所以希望盡量隔離,這里可能就要分析各個應用加載的資源和加載順序之間的沖突,針對這些沖突再自己定些規則,讓它們能夠愉快地玩耍。
- 資源回收。如果你不了解java是如何加載資源的,又怎么理解java是如何回收資源的?
what?
一般說到java的類加載機制,都要說到“雙親委派模型”(其實個人很不理解為什么叫“雙親”,其實英文叫“parent”)。使用這種機制,可以避免重復加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。JVM根據 類名+包名+ClassLoader實例ID 來判定兩個類是否相同,是否已經加載過(所以這里可以略微擴展下,可以通過創建不同的classloader實例來實現類的熱部署)。
有個圖很形象(來源見參考資料)。
注意:如果想形象地看到java的類加載順序,可以在運行java的時候加個啟動參數,即java –verbose
下面結合上圖來詳細介紹下java的類加載過程。
- BootStrapClassLoader。它是最頂層的類加載器,是由C++編寫而成, 已經內嵌到JVM中了。在JVM啟動時會初始化該ClassLoader,它主要用來讀取Java的核心類庫JRE/lib/rt.jar中所有的class文件,這個jar文件中包含了java規范定義的所有接口及實現。
- ExtensionClassLoader。它是用來讀取Java的一些擴展類庫,如讀取JRE/lib/ext/*.jar中的包等(這里要注意,有些版本的是沒有ext這個目錄的)。
- AppClassLoader。它是用來讀取CLASSPATH下指定的所有jar包或目錄的類文件,一般情況下這個就是程序中默認的類加載器。
- CustomClassLoader。它是用戶自定義編寫的,它用來讀取指定類文件 。基于自定義的ClassLoader可用于加載非Classpath中(如從網絡上下載的jar或二進制)的jar及目錄、還可以在加載前對class文件優一些動作,如解密、編碼等。
很多資料和文章里說,ExtClassLoader的父類加載器是BootStrapClassLoader,其實這里省掉了一句話,容易造成很多新手(比如我)的迷惑。嚴格來說,ExtClassLoader的父類加載器是null,只不過在默認的ClassLoader 的 loadClass 方法中,當parent為null時,是交給BootStrapClassLoader來處理的,而且ExtClassLoader 沒有重寫默認的loadClass方法,所以,ExtClassLoader也會調用BootStrapLoader類加載器來加載,這就導致“BootStrapClassLoader具備了ExtClassLoader父類加載器的功能”。看一下下面的代碼就很容易理解上面這一大段話了。
/*** 查看父類加載器*/ private static void test1() {ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();System.out.println("系統類裝載器:" + appClassLoader);ClassLoader extensionClassLoader = appClassLoader.getParent();System.out.println("系統類裝載器的父類加載器——擴展類加載器:" + extensionClassLoader);ClassLoader bootstrapClassLoader = extensionClassLoader.getParent();System.out.println("擴展類加載器的父類加載器——引導類加載器:" + bootstrapClassLoader); }可以看出ExtensionClassLoader的parent為null。
三個重要的方法
查看classloader的源碼可以發現三個重要的方法:
- loadClass。classloader加載類的入口,此方法負責加載指定名字的類,ClassLoader的實現方法為先從已經加載的類中尋找,如沒有則繼續從父ClassLoader中尋找,如仍然沒找到,則從BootstrapClassLoader中尋找,最后再調用findClass方法來尋找,如要改變類的加載順序,則可覆蓋此方法,如加載順序相同,則可通過覆蓋findClass來做特殊的處理,例如解密、固定路徑尋找等,當通過整個尋找類的過程仍然未獲取到Class對象時,則拋出ClassNotFoundException。如類需要resolve,則調用resolveClass進行鏈接。
- findClass。此方法直接拋出ClassNotFoundException,因此需要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類。
- defineClass。此方法負責將二進制的字節碼轉換為Class對象,這個方法對于自定義加載類而言非常重要,如二進制的字節碼的格式不符合JVM Class文件的格式,拋出ClassFormatError;如需要生成的類名和二進制字節碼中的不同,則拋出NoClassDefFoundError;如需要加載的class是受保護的、采用不同簽名的或類名是以java.開頭的,則拋出SecurityException;如需加載的class在此ClassLoader中已加載,則拋出LinkageError。
好,可能有人看了上面會疑惑,為什么上面說自定義classloader是需要重寫findClass而不是loadClass或者其他方法。這里建議查看源碼。
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}可以看到,JDK已經在loadClass方法中幫我們實現了ClassLoader搜索類的判斷方法,當在loadClass方法中搜索不到類時,loadClass方法就會調用findClass方法來搜索類,所以我們只需重寫該方法即可。
看完這一大串可能有人還是不理解,ok,那我們現在就動手寫一個自己的ClassLoader,盡量包含上面三個方法。
自定義ClassLoader
先定義一個Person接口。
public interface Person {public void say(); }再定一個高富帥類實現這個接口
public class HighRichHandsome implements Person {@Overridepublic void say() {System.out.println("I don't care whether you are rich or not");}}好的,開胃菜結束,主角來了,MyClassLoader。
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream;public class MyClassLoader extends ClassLoader{/* * 覆蓋了父類的findClass,實現自定義的classloader*/@Overridepublic Class<?> findClass(String name) {byte[] bt = loadClassData(name);return defineClass(name, bt, 0, bt.length);}private byte[] loadClassData(String className) {InputStream is = getClass().getClassLoader().getResourceAsStream(className.replace(".", "/") + ".class");ByteArrayOutputStream byteSt = new ByteArrayOutputStream();int len = 0;try {while ((len = is.read()) != -1) {byteSt.write(len);}} catch (IOException e) {e.printStackTrace();}return byteSt.toByteArray();} }代碼很簡單,不解釋了,最后在測試類LoaderTest里寫個測試方法。
/*** 父類classloader* @throws Exception*/ private static void test2() throws Exception{MyClassLoader loader = new MyClassLoader();Class<?> c = loader.loadClass("com.alibaba.classload.HighRichHandsome");System.out.println("Loaded by :" + c.getClassLoader());Person p = (Person) c.newInstance();p.say();HighRichHandsome man = (HighRichHandsome) c.newInstance();man.say(); }main方法中調用這個方法即可。LoaderTest默認構造函數會設置AppClassLoader為parent, 測試時執行test2()方法會發現HighRichHandsome類是委托AppClassLoader加載的,所以AppClassLoader可以訪問到,不會出錯。
但是我們再想一下,如果我們直接加載,不委托給父類加載,會出現什么情況?
/*** 自己的classloader加載* @throws Exception*/ private static void test3() throws Exception{MyClassLoader loader = new MyClassLoader();Class<?> c = loader.findClass("com.alibaba.classload.HighRichHandsome");System.out.println("Loaded by :" + c.getClassLoader());Person p = (Person) c.newInstance();p.say();//注釋下面兩行則不報錯HighRichHandsome man = (HighRichHandsome) c.newInstance();man.say(); }可以看到,悲劇的報錯了。根據class loader命名空間規則,每個class loader都有自己唯一的命名空間,每個class loader 只能訪問自己命名空間中的類,如果一個類是委托parent加載的,那么加載后,這個類就類似共享的,parent和child都可以訪問到這個類,因為parent是不會委托child加載類的,所以child加載的類parent訪問不到。簡單來說,即子加載器的命名空間包含了parent加載的所有類,反過來則不成立。 本例中LoaderTest類是AppClassLoader加載的,所以其看不見由MyClassLoader加載的HighRichHandsome類,但Person接口是可以訪問的,所以賦給Person類型不會出錯。
一些小的知識點
1.Class.forName()
相信大家都寫過連接數據庫的例子,基本上就是加載驅動,建立連接,創建請求,寫prepareStatement,關閉連接之類的。在這里,有一段代碼:
public DbTest() {try {Class.forName("com.mysql.jdbc.Driver");// 加載驅動conn = DriverManager.getConnection(url, "root", "");// 建立連接stm = conn.createStatement(); // 創建請求} catch (Exception e) {e.printStackTrace();} }我相信大家一開始的時候肯定都有些疑惑,就是Class.forName(“com.mysql.jdbc.Driver”),為什么加載驅動是Class.forName,而不是ClassLoader的loadClass?為什么這么寫就可以加載驅動了呢?
其實Class.forName()是顯示加載類,作用是要求JVM查找并加載指定的類,也就是說JVM會執行該類的靜態代碼段。查看com.mysql.jdbc.Driver源碼可以發現里面有個靜態代碼塊,在加載后,類里面的靜態代碼塊就執行(主要目的是注冊驅動,把自己注冊進去),所以主要目的就是為了觸發這個靜態方法。
2.Web容器加載機制
其實web容器的加載機制這一點我說不好,因為沒看過諸如tomcat之類的源碼,但是根據自己使用感覺以及查了相關資料,可以簡單介紹一下。一般服務器端都要求類加載器能夠反轉委派原則,也就是先加載本地的類,如果加載不到,再到parent中加載。這個得細看,我這里只知道這個概念,所以就不說了。JavaEE規范推薦每個模塊的類加載器先加載本類加載的內容,如果加載不到才回到parent類加載器中嘗試加載。
3.重復加載與回收
一個class可以被不同的class loader重復加載,但同一個class只能被同一個class loader加載一次。見下列代碼:
/*** 對象只加載一次,返回true*/ private static void test4() {ClassLoader c1 = LoaderTest.class.getClassLoader();LoaderTest loadtest = new LoaderTest();ClassLoader c2 = loadtest.getClass().getClassLoader();System.out.println("c1.equals(c2):"+c1.equals(c2)); }類的回收。說到最開始的why,其實java的回收機制我前面有篇博客說的比較詳細,這里就插一句廢話吧,當某個classloader加載的所有類實例化的所有對象都被回收了,則該classloader會被回收。
參考:http://blog.csdn.net/xyang81/article/details/7292380
ps:話說csdn的markdown寫出來的效果實在不敢恭維,小小的吐槽一下。
總結
以上是生活随笔為你收集整理的深入分析ClassLoader的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 办公数字化选钉钉还是企业微信?
- 下一篇: 我的世界显示服务器领地指令,我的世界服务