Java虛擬機知識整理——類加載器

什麼是類加載器

虛擬機設計團隊把類加載階段中“通過一個類的全限定名來描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類,實現這個動作的代碼模塊稱爲“類加載器”。
類加載器可以說是Java語言的一項創新。也是Java語言流行的重要原因之一,它最初是爲了滿足JavaApplet的需求而開發出來的。雖然目前JavaApplet技術已經基本死掉,但類加載器卻在類層次劃分、OSGi、熱部署、代碼加密等領域大放異彩成爲了Java技術體系中一塊重要的基石,可謂是失之桑榆,收之東隅。

類與類加載器

類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以表達得更加通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下次啊遊意義,否則,及時這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
這裏所指的“相等”,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字作對象所屬關係判定等情況。古國沒有注意到類加載器的影響,在某些情況下可能會產生具有迷惑性的結果。

雙親委派模型

從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器,這類加載器使用C++語言實現,是虛擬機自身的一部分;另一種就是所用其他的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全部都繼承自抽象類java.langClassLoader.
從Java開發人員的角度來看,類加載器還可以華爲得更細緻一點,絕大部分Java程序都會用到以下3中系統提供的類加載器:

  • 啓動類加載器:這個類加載器負責將存放在< JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名稱識別,名字不符合的類庫及時放在lib目錄中也不會被加載)類庫加載到虛擬機內存中,啓動類加載器無法被Java程序直接引用,用戶在編寫自動以類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用null代替即可。
  • 擴展類加載器:這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載< JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
  • 應用類加載器:這個類加載器由sun.misc.Launcher$AppClassLoader實現。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它爲系統類加載器,它負責加載用戶類路徑上指定的類庫(ClassPath),開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過直接的類加載器,一般情況下這個就是程序中默認的類加載器。
    我們的應用程序都是由這3種類加載器相互配合進行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器的層次順序如下:啓動類加載器,擴展類加載器,應用程序類加載器,自定義加載器。
    這種類加載器之間的層次關係稱爲類加載器的雙親委派模型。雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當有之間的父類加載器。這裏類加載器之間的父子關係一般不會一繼承的關係來實現,而是都使用組合關係來服用父加載器的代碼。
    雙親委派模型的工作過程是:如果一個雷加載器收到了類加載的請求,它首先不會之間嘗試加載這個類,而是把這個請求委派給父類加載器去完全,每一層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋之間無法完成這個加載請求時(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試之間去加載。
    使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是Java類隨着它的類加載器一起舉杯了一種帶有優先級的層次關係。如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶之間編寫了一個稱爲java.lang.Object的類,並放在ClassPath中,那系統中會出現多個不同的Object類,Java類型體系中最基礎的行爲也就無法保證,應用程序也將會變得一片混亂。
    雙親委派模型對於保證Java程序的穩定運作很重要,但它的實現卻非常簡單,實現雙親蚊拍的代碼都集中在java.lang.ClassLoader的loadClass()方法之中,邏輯清晰易懂:先檢查是否已經被加載過,若沒有加載則調用父類加載器的loadClass()方法,若父類加載器爲空則默認使用啓動類加載器作爲類加載器。如果父類加載失敗,拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。

破壞雙親委派模型

上面提到的雙親委派模型並不是一個強制性的約束模型,而是Java設計者推薦給開發者的類加載器實現方式。在Java的世界中大部分的類加載器都遵循這個模型,但也有例外,到目前爲止,雙親委派模型主要出現過3次較大規模的“被破壞”情況。
雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前。由於雙親委派模型在JDK1.2之後才被引入,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0的時代已經存在,面對已經存在的用戶自定義類加載器的實現代碼,Java設計者引入雙親委派模型時不得不做一些妥協。爲了向前兼容,JDK1.2之後的java.lang.ClassLoader添加了一個新的protected方法findClass(),在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是爲了重寫loadClass()方法,因爲虛擬機在進行類加載的時候回調用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調用之間的loadClass()。
所以在JDK1.2之後已經不再提倡用戶再去覆蓋loadClass()方法,而映帶把之間的類加載邏輯寫到findClass()方法中,在loadClass()方法中的邏輯裏如果父類加載失敗,則會調用之間的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派規劃的。

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷所導致的,雙親委派很好得解決了各個類加載器的基礎類統一問題(越基礎的類由越上層的加載類進行加載),基礎類之所有被稱爲“基礎”,是因爲他們總是作爲被用戶代碼調用的API,但世事沒有絕對的完美,如果基礎類又要調用回用戶的代碼,那怎麼辦?
爲了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器。這類加載器可以通過java.lang.Thread類的setContextClassLoaser()方法進行設置,如果穿件線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。有了線程上下文類加載器,就可以做一些“舞弊”的事情了JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載器的動作,這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
雙親委派模型的第三次“被破壞”是由於用戶對程序對泰興的追求而導致的,這裏所說的“動態性”指的是當前一些非常“熱門”的名詞:代碼熱替換、模塊代碼熱部署等,說白了就是希望應用程序能像我們的計算機外設那樣,街上鼠標、U盤,不用重啓機器就能激勵使用,鼠標有問題或要升級就換個鼠標,不用停機也不用重啓。對於個人計算機來說,重啓一次其實沒什麼大不了的,但對於一些生產系統來說,關機重啓一次可能就要被列爲生產事故,這種情況下熱部署就對軟件開發者,尤其是企業級軟件開發者具有很大的吸引力。
在ISGi環境下,類加載不再是雙親委派模型中的樹狀結構,而是進一步發展爲更加複雜的網狀結構,當收到類加載請求時,OSGi將按照下面的順序進行類搜索。
1. 將以java.*開頭的類委派給父類加載器加載。
2. 否則,將委派列表名單內的類委派給父類加載器加載。
3. 否則,將Import列表中的類委派給Export這個類的Bundle的類加載器加載。
4. 否則,查找當前Bundle的ClassPath,使用自己的類加載加載。
5. 否則,查找類是否在自己的FragmentBundle中,如果在,委派給FramentBundle的類加載器加載。
6. 否則,查找DynamicImport列表的Bundle,委派給對應Bundle的類加載器加載。
7. 否則,類查找失敗。
上面的查找順序中只有開頭兩點仍然付哈雙親委派規則,其餘的類查找都是在評級的類加載器中進行的。
這裏雖然是用“被破壞”這個詞類形容上述不符合雙親委派模型原則的行爲,但這裏的“被破壞”並沒有貶義的感情色彩。只要有足夠意義和理由,突破已有原則就可認爲是一種創新,算是另一種意義上的“浴火重生”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章