天天看點

深入了解JVM讀書筆記三: 虛拟機類加載機制

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

7.1概述

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

7.2類加載的時機

類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和解除安裝七個階段。其中驗證、準備和解析三個部分統稱為連接配接,它們開始的順序如下圖所示:

深入了解JVM讀書筆記三: 虛拟機類加載機制

其中類加載的過程中加載、驗證、準備、初始化、解除安裝五個階段發生的順序是确定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援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)所指定的類,開發者可以直接使用該類加載器,如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中預設的類加載器。

    應用程式都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。

  • 深入了解JVM讀書筆記三: 虛拟機類加載機制

這種層次關系稱為類加載器的雙親委派模型。

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

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

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

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

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