Java 類加載器

每個編寫的”.java”拓展名類文件都存儲着需要執行的程序邏輯,這些”.java”文件經過Java編譯器編譯成拓展名爲”.class”的文件,”.class”文件中保存着Java代碼經轉換後的虛擬機指令,當需要使用某個類時,虛擬機將會加載它的”.class”文件,並創建對應的class對象,將class文件加載到虛擬機的內存,這個過程稱爲類加載。

簡單的說,類加載器(class loader)就是用來加載 Java 類到 Java 虛擬機中去的。

1. 類加載器類型

1.1 系統提供的類加載器

系統提供的類加載器主要有以下幾個:

  • 引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader

bootstrap class loader不是Java類,因此它不需要被別人加載,它嵌套在Java虛擬機內核裏面,也就是JVM啓動的時候Bootstrap就已經啓動,它是用C++寫的二進制代碼(不是字節碼)

  • 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
  • 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。

注意:類加載器的體系並不是“繼承”體系,而是委派體系,大多數類加載器首先會到自己的parent中查找類或者資源,如果找不到纔會到自己本地查找。類加載器的委託行爲動機是爲了避免相同的類被加載多次。

除了系統提供的類加載器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的需求。

1.2 雙親委派機制

雙親委派模式要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。

雙親委派模式是在Java 1.2後引入的,其工作原理的是,如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器纔會嘗試自己去加載,這就是雙親委派模式。

優點
1. Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係,通過這種層級關可以避免類的重複加載
2. 其次是安全因素,防止核心API庫被隨意篡改

1.3 委託機制的意義 — 防止內存中出現多份同樣的字節碼

比如兩個類A和類B都要加載System類:

  • 如果不用委託而是自己加載自己的,那麼類A就會加載一份System字節碼,然後類B又會加載一份System字節碼,這樣內存中就出現了兩份System字節碼。
  • 如果使用委託機制,會遞歸的向父類查找,也就是首選用Bootstrap嘗試加載,如果找不到再向下。這裏的System就能在Bootstrap中找到然後加載,如果此時類B也要加載System,也從Bootstrap開始,此時Bootstrap發現已經加載過了System那麼直接返回內存中的System即可而不需要重新加載,這樣內存中就只有一份System的字節碼了。

2. 類加載過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段,其中驗證、準備、解析三個部分統稱鏈接。

這些階段通常都是互相交叉的混合式進行的,通常會在一個階段執行的過程中調用或激活另外一個階段。

這裏簡要說明下Java中的綁定:綁定指的是把一個方法的調用與方法所在的類(方法主體)關聯起來,對java來說,綁定分爲靜態綁定和動態綁定:

  • 靜態綁定:即前期綁定。在程序執行前方法已經被綁定,此時由編譯器或其它連接程序實現。針對java,簡單的可以理解爲程序編譯期的綁定。java當中的方法只有final,static,private和構造方法是前期綁定的。
  • 動態綁定:即晚期綁定,也叫運行時綁定。在運行時根據具體對象的類型進行綁定。在java中,幾乎所有的方法都是後期綁定的。

2.1 加載

加載階段是“類加載機制”中的一個階段,這個階段通常也被稱作“裝載”,主要完成:

  1. 通過一個類的全限定名來獲取其定義的二進制字節流。
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在Java堆中生成一個代表這個類的java.lang.Class對象,作爲對方法區中這些數據的訪問入口。

相對於類加載過程的其他階段,加載階段(準確地說,是加載階段中獲取類的二進制字節流的動作)是開發期可控性最強的階段,因爲加載階段可以使用系統提供的類加載器(ClassLoader)來完成,也可以由用戶自定義的類加載器完成,開發人員可以通過定義自己的類加載器去控制字節流的獲取方式。

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式有虛擬機實現自行定義,虛擬機並未規定此區域的具體數據結構。然後在java堆中實例化一個java.lang.Class類的對象,這個對象作爲程序訪問方法區中的這些類型數據的外部接口。

2.2 驗證

驗證的目的是爲了確保Class文件中的字節流包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。不同的虛擬機對類驗證的實現可能會有所不同,但大致都會完成以下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。

  • 文件格式的驗證:驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區之內。經過該階段的驗證後,字節流纔會進入內存的方法區中進行存儲,後面的三個驗證都是基於方法區的存儲結構進行的。
  • 元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合Java語法規範的元數據信息。
  • 字節碼驗證:該階段驗證的主要工作是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行爲。
  • 符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化爲直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。

2.3 準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。

對於該階段有以下幾點需要注意:

  1. 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。
  2. 這裏所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
  3. 如果類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變量value就會被初始化爲ConstValue屬性所指定的值。

這裏還需要注意如下幾點:

  • 對基本數據類型來說,對於類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來說,在使用前必須顯式地爲其賦值,否則編譯時不通過。
  • 對於同時被static和final修飾的常量,必須在聲明的時候就爲其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地爲其賦值,也可以在類初始化時顯式地爲其賦值,總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值。
  • 對於引用數據類型reference來說,如數組引用、對象引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null。
  • 如果在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。

2.4 解析

解析階段是虛擬機將常量池中的符號引用轉化爲直接引用的過程。

  1. 類或接口的解析:判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,從而進行不同的解析。
  2. 字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束。
  3. 類方法解析:對類方法的解析與對字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對類方法的匹配搜索,是先搜索父類,再搜索接口。
  4. 接口方法解析:與類方法解析步驟類似,知識接口不會有父類,因此,只遞歸向上搜索父接口就行了。

2.5 初始化

初始化是類加載過程的最後一步,到了此階段,才真正開始執行類中定義的Java程序代碼。在準備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序指定的主觀計劃去初始化類變量和其他資源,或者可以從另一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

在以下四種情況下初始化過程會被觸發執行:

  1. 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需先觸發其初始化。生成這4條指令的最常見的java代碼場景是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用類的靜態方法的時候;
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候;
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化、則需要先出發其父類的初始化;
  4. jvm啓動時,用戶指定一個執行的主類(包含main方法的那個類),虛擬機會先初始化這個類。

2.6 總結

整個類加載過程中,除了在加載階段用戶應用程序可以自定義類加載器參與之外,其餘所有的動作完全由虛擬機主導和控制。到了初始化纔開始執行類中定義的Java程序代碼(亦及字節碼),但這裏的執行代碼只是個開端,它僅限於()方法。類加載過程中主要是將Class文件(準確地講,應該是類的二進制字節流)加載到虛擬機內存中,真正執行字節碼的操作,在加載完成後才真正開始。


參考

  1. JVM(三):類加載機制(類加載過程和類加載器)
  2. 深入理解java虛擬機—雙親委派模型
  3. 關於Java類加載雙親委派機制的思考(附一道面試題)
  4. 深入探討 Java 類加載器
發佈了53 篇原創文章 · 獲贊 5 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章