類加載器知識點梳理

類加載器

定義

虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱爲”類加載器“。

類加載器可以說是java語言的一項創新,也是java語言流行的重要原因之一。

1、類與類加載器

類加載器雖然只用於實現類的加載動作,但它在java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名空間。 這句話可以表達的更通俗一些:比較兩個類是否”相等“,只有在這兩個類是由同一個類加載的前提下才有意義,否則,即時這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那麼這兩個類就必定不相等。

2、雙親委派模型

定義

從java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另外一種就是所有其他的類加載器,這些類加載器都由java語言實現,獨立於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader。

從java開發人員的角度來看,覺大部分java程序都會使用到以下3種系統提供的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如tr.jar,名字不符合的類庫即時放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被java程序直接引用,用戶在編寫自定義類加載器時,如果需要吧加載請求委派給引導類加載器,那直接使用null代替即可。如:

圖片

  • 擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
  • 應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$AppClassLoader實現。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱爲系統類加載器。它負責加載用戶路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

圖片

圖7-2中展示的類加載器之間的這種層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏類加載器之間的父子關係一般不會以繼承的關係來實現,而是都使用組合關係來複用父加載器的代碼。

注意:類加載器的雙親委派模型在jdk1.2期間被引入並廣泛應用於之後幾乎所有的java程序中,但它並不是一個強制性的約束模型,而是java設計者推薦給開發者的一種類加載器實現方式。

雙親委派模型的工作過程

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。

優點

使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪一個類加載器都要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱爲java.lang.Object的類,並放在程序的ClassPath中,那系統中將會出現多個不同的Object類,java類型體系中最基礎的行爲也就無法保證,應用程序也將會變得一片混亂。

雙親委派模型對於保證java程序的穩定運作很重要,但它的實現卻是非常簡單,實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法中,如代碼清單7-10所示,邏輯清晰易懂:先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父類加載失敗,拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。圖片

3、破壞雙親委派模型

雙親委派模型並不是一個強制性的約束模型,而java設計者推薦個開發者的類加載器實現方式。在java的世界中大部分的類加載器都遵循這個模型,但也有例外,到目前爲止,雙親委派模型主要出現過3次較大規模的”被破壞“情況。

第一次”被破壞“

發生在雙親委派模型出現之前——即jdk1.2發佈之前。由於雙親委派模型在jdk1.2之後才被引入,而類加載器和抽象類java.lang.ClassLoader則在jdk1.0時代就已經存在,面對已經存在的用戶自定義類加載器的實現代碼,java設計者引入雙親委派模型時不得不做出一些妥協。爲了向前兼容,jdk1.2之後的java.lang.ClassLoader添加了一個新的protected方法findClass(),在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是爲了重寫loadClass()方法,因爲虛擬機在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調用自己的loadClass()。

上面我們已經看過loadClass()方法的代碼,雙親委派的具體邏輯就實現在這個方法之中,jdk1.2之後已不提倡用戶再去覆蓋loadClass()方法,而應當把自己的類加載邏輯寫到findClass()方法中。

第二次”被破壞“

雙親委派模型的第二次”被破壞“是由這個模型自身的缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以稱爲”基礎“,是因爲他們總是作爲被用戶代碼調用的API,但世事往往沒有絕對的完美,如果基礎類又要調用回用戶的代碼,那該怎麼辦?

這並非是不可能的事情,一個典型的例子便是JNDI服務,JNDI現在已經是java的標準服務,它的代碼由啓動類加載器去加載(在JDK1.3時放進去的rt.jar),但是JNDI的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,但啓動類加載器不可能”認識“這些代碼。

爲了解決這個問題,java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。有了線程上下文類加載器,JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。java中所有設計spi的加載動作基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB、JBI等。

第三次”被破壞“

雙親委派模型的第三次”被破壞“是由於用戶對程序動態性的追求而導致的,這裏所說的”動態性“指的是當前一些非常”熱門“的名詞:代碼熱替換(HotSwap)、模塊熱部署(HotSpot Deployment)等。

目前OSGI已經成爲了業界”事實上“的java模塊化標準,而OSGI實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每一個程序模塊(OSGI中稱爲Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。

在OSGI換進下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展爲更加複雜的網狀結構,當收到類加載請求時,OSGI將按照下面的順序進行類搜索:

  1. 將以java.*開頭的類委派給父類加載器加載
  2. 否則,將委派列表名單內的類委派給父類加載器加載
  3. 否則,將Import列表中的類委派給Export這個類的Bundle的類加載器加載
  4. 否則,查找當前Bundle的ClassPath,使用自己的類加載器加載
  5. 否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載
  6. 否則,查找Dynamic Import列表的Bundle,委派給對應Bundle的類加載器加載
  7. 否則,類查找失敗。

上面雖然使用了”被破壞“這個詞來形容上述不符合雙親委派模型原則的行爲,但這裏”被破壞“並不帶有貶義的感情色彩。只要有足夠的意義和理由,突破已有的原則就可認爲是一種創新。

–摘自《深入理解Java虛擬機》

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