(深入理解 Java虛擬機器)一篇文章帶你深入瞭解Java 虛擬機器類載入器

南淮北安發表於2020-11-15

一、類載入器

Java虛擬機器設計團隊有意把類載入階段中的“通過一個類的全限定名來獲取描述該類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需的類。實現這個動作的程式碼被稱為“類載入器”(ClassLoader)。

二、類與載入器

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

這裡所指的“相等”,包括代表類的Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括了使用instanceof關鍵字做物件所屬關係判定等各種情況。如果沒有注意到類載入器的影響,在某些情況下可能會產生具有迷惑性的結果,程式碼清單7-8中演示了不同的類載入器對instanceof關鍵字運算的結果的影響。

三、雙親委派模型

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

站在Java開發人員的角度來看,類載入器就應當劃分得更細緻一些。自JDK 1.2以來,Java一直保持著三層類載入器、雙親委派的類載入架構,儘管這套架構在Java模組化系統出現後有了一些調整變動,但依然未改變其主體結構。本節內容將針對JDK 8及之前版本的Java來介紹什麼是三層類載入器,以及什麼是雙親委派模型。對於這個時期的Java應用,絕大多數Java程式都會使用到以下3個系統提供的類載入器來進行載入。

其他可以參考:一篇文章帶你深入理解 Java 中的Class.getClassLoader

(1)啟動類載入器(Bootstrap Class Loader):前面已經介紹過,這個類載入器負責載入存放在<JAVA_HOME>\lib目錄,或者被-Xbootclasspath引數所指定的路徑中存放的,而且是Java虛擬機器能夠識別的(按照檔名識別,如rt.jartools.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器的記憶體中。啟動類載入器無法被Java程式直接引用,使用者在編寫自定義類載入器時,如果需要把載入請求委派給引導類載入器去處理,那直接使用null代替即可,程式碼清單7-9展示的就是java.lang.ClassLoader.getClassLoader()方法的程式碼片段,其中的註釋和程式碼實現都明確地說明了以null值來代表引導類載入器的約定規則。

(2)擴充套件類載入器(Extension Class Loader):這個類載入器是在類sun.misc.Launcher$ExtClassLoader中以Java程式碼的形式實現的。它負責載入<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中所有的類庫。根據“擴充套件類載入器”這個名稱,就可以推斷出這是一種Java系統類庫的擴充套件機制,JDK的開發團隊允許使用者將具有通用性的類庫放置在ext目錄裡以擴充套件Java SE的功能,在JDK 9之後,這種擴充套件機制被模組化帶來的天然的擴充套件能力所取代。由於擴充套件類載入器是由Java程式碼實現的,開發者可以直接在程式中使用擴充套件類載入器來載入Class檔案。

(3)應用程式類載入器(Application Class Loader):這個類載入器由sun.misc.Launcher$AppClassLoader來實現。由於應用程式類載入器是ClassLoader類中的getSystem-ClassLoader()方法的返回值,所以有些場合中也稱它為“系統類載入器”。它負責載入使用者類路徑(ClassPath)上所有的類庫,開發者同樣可以直接在程式碼中使用這個類載入器。如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
在這裡插入圖片描述
JDK 9之前的Java應用都是由這三種類載入器互相配合來完成載入的,如果使用者認為有必要,還可以加入自定義的類載入器來進行擴充,典型的如增加除了磁碟位置之外的Class檔案來源,或者通過類載入器實現類的隔離、過載等功能。這些類載入器之間的協作關係“通常”會如上圖所示。

圖中展示的各種類載入器之間的層次關係被稱為類載入器的“雙親委派模型(Parents Delegation Model)”。雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應有自己的父類載入器。不過這裡類載入器之間的父子關係一般不是以繼承(Inheritance)的關係來實現的,而是通常使用組合(Composition)關係來複用父載入器的程式碼。

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到最頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去完成載入

使用雙親委派模型來組織類載入器之間的關係,一個顯而易見的好處就是Java中的類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都能夠保證是同一個類。反之,如果沒有使用雙親委派模型,都由各個類載入器自行去載入的話,如果使用者自己也編寫了一個名為java.lang.Object的類,並放在程式的ClassPath中,那系統中就會出現多個不同的Object類,Java型別體系中最基礎的行為也就無從保證,應用程式將會變得一片混亂。如果讀者有興趣的話,可以嘗試去寫一個與rt.jar類庫中已有類重名的Java類,將會發現它可以正常編譯,但永遠無法被載入執行。

雙親委派模型對於保證Java程式的穩定運作極為重要,但它的實現卻異常簡單,用以實現雙親委派的程式碼只有短短十餘行,全部集中在java.lang.ClassLoader的loadClass()方法之中,如程式碼清單7-10所示。

在這裡插入圖片描述
這段程式碼的邏輯清晰易懂:先檢查請求載入的型別是否已經被載入過,若沒有則呼叫父載入器的loadClass()方法,若父載入器為空則預設使用啟動類載入器作為父載入器。假如父類載入器載入失敗,丟擲ClassNotFoundException異常的話,才呼叫自己的findClass()方法嘗試進行載入。

四、破壞雙親委派模型

上文提到過雙親委派模型並不是一個具有強制性約束的模型,而是Java設計者推薦給開發者們的類載入器實現方式。在Java的世界中大部分的類載入器都遵循這個模型,但也有例外的情況,直到Java模組化出現為止,雙親委派模型主要出現過3次較大規模“被破壞”的情況。

雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK1.2面世以前的“遠古”時代。由於雙親委派模型在JDK 1.2之後才被引入,但是類載入器的概念和抽象類java.lang.ClassLoader則在Java的第一個版本中就已經存在,面對已經存在的使用者自定義類載入器的程式碼,Java設計者們引入雙親委派模型時不得不做出一些妥協,為了相容這些已有程式碼,無法再以技術手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之後的java.lang.ClassLoader中新增一個新的protected方法findClass(),並引導使用者編寫的類載入邏輯時儘可能去重寫這個方法,而不是在loadClass()中編寫程式碼。

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的,雙親委派很好地解決了各個類載入器協作時基礎型別的一致性問題(越基礎的類由越上層的載入器進行載入),基礎型別之所以被稱為“基礎”,是因為它們總是作為被使用者程式碼繼承、呼叫的API存在,但程式設計往往沒有絕對不變的完美規則,如果有基礎型別又要呼叫回使用者的程式碼,那該怎麼辦呢?

雙親委派模型的第三次“被破壞”是由於使用者對程式動態性的追求而導致的,這裡所說的“動態性”指的是一些非常“熱”門的名詞:程式碼熱替換(Hot Swap)、模組熱部署(Hot Deployment)等。說白了就是希望Java應用程式能像我們的電腦外設那樣,接上滑鼠、U盤,不用重啟機器就能立即使用,滑鼠有問題或要升級就換個滑鼠,不用關機也不用重啟。對於個人電腦來說,重啟一次其實沒有什麼大不了的,但對於一些生產系統來說,關機重啟一次可能就要被列為生產事故,這種情況下熱部署就對軟體開發者,尤其是大型系統或企業級軟體開發者具有很大的吸引力。

相關文章