JVM類載入機制詳解,建議看這一篇就夠了,深入淺出總結的十分詳細!

類載入機制

虛擬機把描述類的數據從Class文件載入到記憶體,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類載入機制。

類載入的時機

  • 遇到new(比如new Student())、getstatic和putstatic(讀取或設置一個類的靜態欄位,如下程式碼,讀取被final修飾並已在編譯器把結果放入常量池的靜態欄位除外)、invokestatic(調用類的靜態方法)這四條指令時,如果對應的類沒有初始化,則要對對應的類先進行初始化。
public class Student{
	private static int age;
	public static void method(){
	}
}
Student.age
Student.method();

  • 使用java.lang.reflect包方法時對類進行反射調用的時候。
  • 初始化一個類的時候發現其父類還沒初始化,要先初始化其父類。
  • 當虛擬機開始啟動時,用戶需要指定一個主類(main),虛擬機會限制性這個主類的初始化。

類載入的過程

類載入過程是如下圖所示的一個流水線過程,其中連接過程可細化為驗證、準備和解析三個小步驟。

載入

class文件–>class對象

「載入」過程主要是靠類載入器實現的,包括用戶自定義類載入器。

載入的過程

在載入過程中,JVM主要做以下3件事:

  1. 通過一個類的全限定名來獲取定義此類的二進位位元組流(class文件)。在程式運行過程中,當要訪問一個類時,若發現這個類尚未被載入,並滿足類初始化的條件時,就根據要被初始化的這個類的全限定名找到該類的二進位位元組流,開始載入過程
  2. 將這個位元組流的靜態存儲結構轉化為方法區的運行時數據結構(即Class對象)
  3. 在記憶體中創建一個該類的java.lang.Class對象,作為方法區該類的各種數據的訪問入口

程式在運行中所有對該類的訪問都通過這個類對象,也就是這個Class對象是提供給外界訪問該類的介面。

載入源

JVM規範對於載入過程給予了較大的寬鬆度,一般二進位位元組流都從已經編譯好的本地class文件中讀取,此外還可以從這些地方讀取:zip包(jar、war、ear等),由jsp文件中生成對應的Class類,資料庫,網路,運行時計算生成(動態代理技術)。

載入過程的注意點

  • 類和數組載入的區別:非數組類是由類載入器來完成;數組類本身不通過類載入器創建,它是由java虛擬機直接創建,但數組類與類載入器有很密切的關係,因為數組類的元素類型最終要靠類載入器創建。
  • HotSpot將Class對象存放在方法區

驗證

各種檢查

驗證階段比較耗時,它非常重要但不一定必要,可用-Xverify:none參數關閉,以縮短類載入時間。

驗證的目的

保證二進位位元組流的資訊符合虛擬機規範,並沒有安全問題。

驗證的必要性

Java語言的安全性是通過編譯器來保證的,但編譯器和虛擬機是兩個獨立的東西,虛擬機只認二進位位元組流,它不會管所獲得的二進位位元組流是哪來的。當然,如果是編譯器給它的那麼就相對安全,但如果是從其它途徑獲得的,那麼無法確保該二進位位元組流是安全的。

驗證的過程

其中文件格式驗證階段是基於二進位位元組流進行的,只有通過本階段驗證,才被允許存放到方法區。後面的三個驗證階段都是基於方法區的存儲結構進行,不會再直接操作位元組流。

準備

為static分配記憶體並初始化0值。JDK1.7之前在方法區,1.7之後在堆。

僅僅為類變數(即static修飾的欄位變數)分配記憶體並且設置該類變數的初始值即零值,這裡不包含用final修飾的static,因為final在編譯的時候就會分配好,同時這裡也不會為實例變數分配初始化。類變數(靜態變數)會分配在方法區中,而實例變數是會隨著對象一起分配到Java堆中。
準備階段主要完成兩件事情:

  • 為已在方法區中的類的靜態成員變數分配記憶體;
  • 為靜態成員變數設置初始值,具體初始值為下圖所示。
    在這裡插入圖片描述

注意:

public static int x = 1000;

實際上變數x在準備階段過後的初始值為0,而不是1000。將x賦值為1000是在初始化階段完成。

解析

將符號引用替換為直接引用

解析是虛擬機將常量池的符號引用替換為直接引用的過程。

初始化

調用方法

初始化過程就是調用類初始化方法的過程,完成對static修飾的類變數的手動賦值還有主動調用靜態程式碼塊。
注意點:此步驟中虛擬機會保證在多執行緒環境中一個類的方法被正確地加鎖(靜態內部類)

類載入器介紹

啟動類載入器:

由C++實現,不是ClassLoader子類。
負責載入JAVA_HOME\lib目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt,jar)的類。

擴展類載入器:

負責載入JAVA_HOME\lib\ext目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。

應用程式類載入器:

負責載入用戶路徑(classpath)上的類庫。

自定義類載入器:

上述的載入器只能載入指定目錄下的jar和class,如果想載入其他位置的jar或類時,則需要實現自定義類載入器來載入。
比如要載入網路上的一個class文件,通過動態載入到記憶體之後,要調用這個類中的方法實現特定業務邏輯,此時默認的ClassLoader就不能滿足我們的需求,需要定義自己的ClassLoader。

雙親委派模型

JVM的類載入是通過多層次的類載入器來完成的,類的層次關係和載入順序可以由下圖來描述:

載入過程中會先檢查類是否被已載入,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader組層檢查,只要某個classloader已載入就視為已載入此類,保證此類只載入一次。而載入的順序是自頂向下,也就是由上層來組層嘗試載入此類。這種類載入的層次關係就是雙親委派模型。
需注意的點:

  • 當一個類載入器收到類載入任務,會先交給其父類載入器去完成,因此最終載入任務都會傳遞到頂層的啟動類載入器;
  • 只有當父類載入器無法完成載入任務時,才會嘗試執行載入任務。

為什麼使用雙親委派這種模型

因為這樣可以避免類的重複載入,當父classloader經載入了該類的時候,就沒必要子classloader再載入一次。

考慮到安全因素,我們試想一下,如果不適用這種委託模型,那我們就可以隨時使用自定義的String來動態替代java核心api重定義的類型,這樣會存在非常大的安全隱患,而雙親委託的方式,就可以避免這種情況。因為String已經在啟動時就被Bootstrap ClassLoader載入,所以用戶自定義的ClassLoader永遠也無法載入一個自己寫的String,除非改編JDK中ClassLoader搜索類的默認演算法。

判定兩個Class對象是否相同的依據

  • class位元組碼是否相同
  • ClassLoader是否相同

JVM在判定兩個class是否相同,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類載入器實例載入的。類的全限定名完全相同,但是載入它的類載入器不同,那麼在方法區中會產生不同的Class對象。
只有兩者同時滿⾜的情況下,JVM才認為這兩個class是相同的。就算兩個class是同⼀份class位元組碼,如果被兩個不同的ClassLoader實例所載入,JVM也會認為它們是兩個不同class。

破壞雙親委派模型

為什麼需要破壞雙親委派

因為在某些情況下父類載入器需要載入的class文件由於受到載入範圍的限制,父類載入器無法載入到需要的文件,這個時候就需要委託子類載入器進行載入。
雙親委派模型是在JDK1.2以後才使用的,但是有一些核心的API類在JDK1.2之前就已經寫好了。

簡單理解雙親委派模型是子類載入器去委託父類載入器完成類載入的工作,而破壞雙親委派模型是父類載入器去委託子類載入器完成類載入的工作。

以Driver介面為例,由於Driver介面定義在jdk當中,而其實現由各個資料庫的服務商來提供,比如mysql就寫了MySQL Connector,這些實現類都是以jar包的形式放到classpath目錄下。
那麼問題就來了,DriverManager(也由jdk提供,JDK1.2之前就寫好了)要載入各個實現了Driver介面的實現類(在classpath下),然後進行管理,但是DriverManager由啟動類載入器載入,只能載入JAVA_HOME\lib下的文件,而其實現是由服務商提供的,有系統類載入器載入,這個時候就需要啟動類載入器來委託子類載入器來載入Driver實現,從而破壞了雙親委派。如下圖所示。

最後

歡迎關注公眾號:前程有光,領取一線大廠Java面試題總結+各知識點學習思維導+一份300頁pdf文檔的Java核心知識點總結!