深入理解JVM讀書筆記三: 虛擬機器類載入機制

衣舞晨風發表於2016-10-18

Java虛擬機器類載入機制是把Class類檔案載入到記憶體,並對Class檔案中的資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別的過程。

7.1概述

與那些在編譯時需要進行連結工作的語言不同,在Java語言裡面,型別的載入和連結過程都是在程式執行期間完成的(其實C++也是分為靜態連結庫和動態連結庫的),這樣會在類載入時稍微增加一些效能開銷,但是卻能為Java應用程式提供高度的靈活性,Java中天生可以動態擴充套件的語言特性就是依賴執行期動態載入和動態連結這個特點實現的。

7.2類載入的時機

類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括:載入驗證準備解析初始化使用解除安裝七個階段。其中驗證、準備和解析三個部分統稱為連線,它們開始的順序如下圖所示:
這裡寫圖片描述

其中類載入的過程中載入驗證準備初始化解除安裝五個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)。另外注意這裡的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中呼叫或啟用另一個階段。

這裡簡要說明下Java中的繫結:繫結指的是把一個方法的呼叫與方法所在的類(方法主體)關聯起來,對java來說,繫結分為靜態繫結和動態繫結:

  • 靜態繫結:即前期繫結。在程式執行前方法已經被繫結,此時由編譯器或其它連線程式實現。針對java,簡單的可以理解為程式編譯期的繫結。java當中的方法只有final,static,private和構造方法是前期繫結的。

  • 動態繫結:即晚期繫結,也叫執行時繫結。在執行時根據具體物件的型別進行繫結。在java中,幾乎所有的方法都是後期繫結的。

虛擬機器規範嚴格規定了有且只有5種情況必須立即對類進行初始化:(稱為對類的主動引用

(1)遇到new、getstatic、putstatic和invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發器初始化。
生成這四條指令最常見的 Java 程式碼場景是:
使用 new 關鍵字例項化物件時、讀取或設定一個類的靜態欄位(static)時(被 static 修飾又被 final 修飾的,已在編譯期把結果放入常量池的靜態欄位除外)、以及呼叫一個類的靜態方法時。

(2)使用java.lang.reflect包的方法對類進行反射呼叫的時候。

(3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

(4)當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。

(5)當使用jdk1.7的動態語言進行支援的時候,如果一個java.lang.invoke.methodHandle例項最後的解析結果ref_getstatic等的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行初始化,則需要先觸發其初始化。

除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用:

(1)通過子類引用父類的靜態欄位,不會導致子類初始化,對於靜態欄位,只有直接定義這個欄位的類才會被初始化。

(2)通過陣列定義來引用類,不會觸發此類的初始化。

(3)常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

介面的載入和類的載入稍有一些不同,但是介面也有初始化的過程,這一點與類是一致的,編譯器會為介面生成“()”類構造器,用於初始化介面中定義的成員變數,真正的區別是類在初始化的過程中要求其父類全部都已經初始化過了,但是一個介面在初始化時,並不要求其父介面全部都進行了初始化,只有在真正使用到了父介面的時候才會初始化

7.3類載入的過程

載入時類載入過程的第一個階段,在載入階段,虛擬機器需要完成以下三件事情:

  1. 通過一個類的全限定名來獲取其定義的二進位制位元組流。
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
  3. 在Java堆中生成一個代表這個類的java.lang.Class物件,作為對方法區中這些資料的訪問入口。

注意,這裡第1條中的二進位制位元組流並不只是單純地從Class檔案中獲取,比如它還可以從Jar包中獲取、從網路中獲取(最典型的應用便是Applet)、由其他檔案生成(JSP應用)等。

相對於類載入的其他階段而言,載入階段(準確地說,是載入階段獲取類的二進位制位元組流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類載入器來完成載入,也可以自定義自己的類載入器來完成載入。

載入階段完成後,虛擬機器外部的 二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,而且在Java堆中也建立一個java.lang.Class類的物件,這樣便可以通過該物件訪問方法區中的這些資料。

7.4類載入器

7.4.1類與類載入器

類載入器雖然只用於實現類的載入動作,但它在Java程式中起到的作用卻遠遠不限於類的載入階段。對於任意一個類,都需要由它的類載入器和這個類本身一同確定其在就Java虛擬機器中的唯一性,也就是說,即使兩個類來源於同一個Class檔案,只要載入它們的類載入器不同,那這兩個類就必定不相等。

這裡的“相等”包括了代表類的Class物件的equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用instanceof關鍵字對物件所屬關係的判定結果。

7.4.2雙親委派模型

站在Java虛擬機器的角度來講,只存在兩種不同的類載入器:

  • 啟動類載入器:它使用C++實現(這裡僅限於Hotspot,也就是JDK1.5之後預設的虛擬機器,有很多其他的虛擬機器是用Java語言實現的),是虛擬機器自身的一部分。
  • 所有其他的類載入器:這些類載入器都由Java語言實現,獨立於虛擬機器之外,並且全部繼承自抽象類java.lang.ClassLoader,這些類載入器需要由啟動類載入器載入到記憶體中之後才能去載入其他的類。

站在Java開發人員的角度來看,類載入器可以大致劃分為以下三類:

  • 啟動類載入器:Bootstrap ClassLoader,跟上面相同。它負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader載入)。啟動類載入器是無法被Java程式直接引用的。
  • 擴充套件類載入器:Extension ClassLoader,該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充套件類載入器。
  • 應用程式類載入器:Application ClassLoader,該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
    應用程式都是由這三種類載入器互相配合進行載入的,如果有必要,我們還可以加入自定義的類載入器。
    這裡寫圖片描述

這種層次關係稱為類載入器的雙親委派模型

雙親委派模型要求除了啟動類載入器之外,其餘的類載入器都應當有自己的父類載入器,這種父子關係一般用組合實現:一個類載入器收到類載入請求時,首先委託給父類去載入,父類又遞迴地委託給自己的父類去載入,只有在父類無法載入時才自己嘗試去載入。

類載入器雙親委派模型是從JDK1.2以後引入的,並且只是一種推薦的模型,不是強制要求的。

適當地不遵守雙親委派模型可以實現一些特殊的類載入需求,比如熱部署。

雙親委派 模式的類載入機制的優點是java類它的類載入器一起具備了一種帶優先順序的層次關係,越是基礎的類,越是被上層的類載入器進行載入,保證了java程式的穩定執行。

相反, 如果沒有使用雙親委派模型,由各個類載入器自行去載入的話。如果使用者編寫了一個稱為“java.lang.Object”的類,並存放在程式的ClassPath中,那系統中將會出現多個不同的Object類,java型別體系中最基礎的行為也就無法保證。應用程式也將會一片混亂。

深入理解Java虛擬機器——JVM高階特性與最佳實踐(第2版)PDF版下載:
http://download.csdn.net/detail/xunzaosiyecao/9648998

作者:jiankunking 出處:http://blog.csdn.net/jiankunking

相關文章