一文教你讀懂JVM的類載入機制
阿新 • • 發佈:2020-09-02
Java執行程式又被稱為WORA(Write Once Run Anywhere,在任何地方執行只需寫入一次),意味著我們程式設計師小哥哥可以在任何一個系統上開發Java程式,但是卻可以在所有系統上暢通執行,無需任何調整,大家都知道這是JVM的功勞,但具體是JVM的哪個模組或者什麼機制實現這一功能呢?
JVM(Java Virtual Machine, Java虛擬機器)作為執行java程式的執行時引擎,也是JRE(Java Runtime Environment, Java執行時環境)的一部分。
說起它想必不少小夥伴任處於似懂非懂的狀態吧,說實話,著實是塊難啃的骨頭。但古語有云:千里之行,始於足下。我們今天主要談談,為什麼JVM無需瞭解底層檔案或者檔案系統即可執行Java程式?
--這主要是類載入機制在執行時將Java類動態載入到JVM的緣故。
當我們編譯.java檔案時,Java編譯器會生成與.java檔案同名的.class檔案(包含位元組碼)。當我們執行時,.class檔案會進入到各個步驟,這些步驟共同描繪了整個JVM,上圖便是一張精簡的JVM架構圖。
今天,我們的主角就是類載入機制 - 說白了,就是將.class檔案載入到JVM記憶體中,並將其轉化為java.lang.Class物件的過程。這對這個過程,我們可以細分為如下幾個階段:
StringOp.class
注意:對於每個載入的.class檔案,僅會建立一個java.lang.Class物件.
作者:吳家二少 部落格地址:https://www.cnblogs.com/cloudman-open/ 本文歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線
- 載入
- 連線(驗證,準備,解析)
- 初始化
I.類載入流程
1. 載入
載入:通過類的全侷限定名找到.class檔案,並利用.class檔案建立一個java.lang.Class物件。- 根據類的全侷限定名找到.class檔案,生成對應的二進位制位元組流。
- 將靜態儲存結構轉換為執行時資料結構,儲存執行時資料結構到JVM記憶體方法區中。
- JVM建立java.lang.Class型別的物件,保存於堆(Heap)中。利用該物件,可以獲取保存於方法區中的類資訊,例如:類名稱,父類名稱,方法和變數等資訊。
package com.demo; import java.lang.reflect.Field; import java.lang.reflect.Method; public class ClassLoaderExample { public static void main(String[] args) { StringOp stringOp = new StringOp(); System.out.println("Class Name: " + stringOp.getClass().getName()); for(Method method: stringOp.getClass().getMethods()) { System.out.println("Method Name: " + method.getName()); } for (Field field: stringOp.getClass().getDeclaredFields()) { System.out.println("Field Name: " + field.getName()); } } }
package com.demo; public class StringOp { private String displayName; private String address; public String getDisplayName() { return displayName; } public String getAddress() { return address; } }
output:
Class Name: com.demo.StringOp Method Name: getAddress Method Name: getDisplayName Field Name: displayName Field Name: address
StringOp stringOp1 = new StringOp(); StringOp stringOp2 = new StringOp(); System.out.println(stringOp1.getClass() == stringOp2.getClass()); //output: true
2. 連線
2.1 驗證
驗證:主要是確保.class檔案的正確性,由有效的編譯器生成,不會對影響JVM的正常執行。通常包含如下四種驗證:- 檔案格式:驗證檔案的格式是否符合規範,如果符合規範,則將對應的二進位制位元組流儲存到JVM記憶體的方法區中;否則丟擲java.lang.VerifyError異常。
- 元資料:對位元組碼的描述資訊進行語義分析,確保符合Java語言規範。例如:是否有父類;是否繼承了不允許繼承的類(final修飾的類);如果是實體類實現介面,是否實現了所有的方法;等。。
- 位元組碼:驗證程式語義是否合法,確保目標類的方法在被呼叫時不會影響JVM的正常執行。例如int型別的變數是否被當成String型別的變數等。
- 符號引用:目標類涉及到其他類的的引用時,根據引用類的全侷限定名(例如:import com.demo.StringOp)能否找到對應的類;被引用類的欄位和方法是否可被目標類訪問(public, protected, package-private, private)。這裡主要是確保後續目標類的解析步驟可以順利完成。
2.2 準備
準備:為目標類的靜態欄位分配記憶體並設定預設初始值(當欄位被final修飾時,會直接賦值而不是預設值)。需要注意的是,非靜態變數只有在例項化物件時才會進行欄位的記憶體分配以及初始化。public class CustomClassLoader { //載入CustomClassLoader類時,便會為var1變數分配記憶體 //準備階段,var1賦值256 public static final int var1 = 256; //載入CustomClassLoader類時,便會為var2變數分配記憶體 //準備階段,var2賦值0, 初始化階段賦值128 public static int var2 = 128; //例項化一個CustomClassLoader物件時,便會為var1變數分配記憶體和賦值 public int var3 = 64; }注意:靜態變數存在方法區記憶體中,例項變數存在堆記憶體中。 這裡簡單貼一下Java不同變數的預設值:
資料型別 | 預設值 |
int | 0 |
float | 0.0f |
long | 0L |
double | 0.0d |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
String | null |
boolean | false |
ArrayList | null |
HashMap | null |
2.3 解析
解析:將符號引用轉化為直接引用的過程。- 符號引用(Symbolic Reference):描述所引用目標的一組符號,使用該符號可以唯一標識到目標即可。比如引用一個類:com.demo.CustomClassLoader,這段字串就是一個符號引用,並且引用的物件不一定事先載入到記憶體中。
- 直接引用(Direct Reference):直接指向目標的指標,相對偏移量或者一個能間接定位到目標的控制代碼。根據直接引用的定義,被引用的目標一定事先載入到了記憶體中。
3. 初始化
前面的準備階段時,JVM為目標類的靜態變數分配記憶體並設定預設初始值(final修飾的靜態變數除外),但到了初始化階段會根據使用者編寫的程式碼重新賦值。換句話說:初始化階段就是JVM執行類構造器方法<clinit>()的過程。 <init>()和<clinit>()從名字上來看,非常的類似,或許某些童鞋會給雙方畫上等號。然則,對於JVM來說,雖然兩者皆被稱為構造器方法,但此構造器非彼構造器。- <init>():物件構造器方法,用於初始化例項物件
- 例項物件的constructor(s)方法,和非靜態變數的初始化;
- 執行new建立例項物件時使用。
- <clinit>():類構造器方法,用於初始化類
- 類的靜態語句塊和靜態變數的初始化;
- 類載入的初始化階段執行。
public class ClassLoaderExample { private static final Logger logger = LoggerFactory.getLogger(ClassLoaderExample.class);//<clinit> private String property = "custom"; //<init> //<clinit> static { System.out.println("Static Initializing..."); } //<init> ClassLoaderExample() { System.out.println("Instance Initializing..."); } //<init> ClassLoaderExample(String property) { this.property = property; System.out.println("Instance Initializing..."); } }檢視對應的位元組碼: public ClassLoaderExample(); <init>
Code: 0 aload_0 //將區域性變量表中第一個引用載入到操作樹棧 1 invokespecial #1 <java/lang/Object.<init>> //呼叫java.lang.Object的例項初始化方法 4 aload_0 //將區域性變量表中第一個引用載入到操作樹棧 5 ldc #2 <custom> //將常量custom從常量池第二個位置推送至棧頂 7 putfield #3 <com/kaiwu/ClassLoaderExample.property> //設定com.kaiwu.ClassLoaderExample例項物件的property欄位值為custom 10 getstatic #4 <java/lang/System.out> //從java.lang.System類中獲取靜態欄位out 13 ldc #5 <Instance Initializing...> //將常量Instance Initializing...從常量池第5個位置推送至棧頂 15 invokevirtual #6 <java/io/PrintStream.println> //呼叫java.io.PrintStream物件的println例項方法,列印棧頂的Instance Initializing... 18 return //返回public ClassLoaderExample(String property); <init>
Code: 0 aload_0 //將區域性變量表中第一個引用載入到操作樹棧 1 invokespecial #1 <java/lang/Object.<init>> //呼叫java.lang.Object的例項初始化方法 4 aload_0 //將區域性變量表中第一個引用載入到操作樹棧 5 ldc #2 <custom> //將常量custom從常量池第二個位置推送至棧頂 7 putfield #3 <com/kaiwu/ClassLoaderExample.property> //將常量custom賦值給com.kaiwu.ClassLoaderExample例項物件的property欄位 10 aload_0 //將區域性變量表中第一個引用載入到操作樹棧 11 aload_1 //將區域性變量表中第二個引用載入到操作樹棧 12 putfield #3 <com/kaiwu/ClassLoaderExample.property> //將入參property賦值給com.kaiwu.ClassLoaderExample例項物件的property欄位 15 getstatic #4 <java/lang/System.out> //從java.lang.System類中獲取靜態欄位out 18 ldc #5 <Instance Initializing...> //將常量Instance Initializing...從常量池第5個位置推送至棧頂 20 invokevirtual #6 <java/io/PrintStream.println> //呼叫java.io.PrintStream物件的println例項方法, 列印棧頂的Instance Initializing... 23 return //返回<clinit>():
Code: 0 ldc #7 <com/kaiwu/ClassLoaderExample> //將com.kaiwu.ClassLoaderEexample的class_info常量從常量池第七個位置推送至棧頂 2 invokestatic #8 <org/slf4j/LoggerFactory.getLogger> //從org.slf4j.LoggerFactory類中獲取靜態欄位getLogger 5 putstatic #9 <com/kaiwu/ClassLoaderExample.logger> //設定com.kaiwu.ClassLoaderExample類的靜態欄位logger 8 getstatic #4 <java/lang/System.out> //從java.lang.System類中獲取靜態欄位out 11 ldc #10 <Static Initializing...> //將常量Static Initializing...從常量池第10個位置推送至棧頂 13 invokevirtual #6 <java/io/PrintStream.println> //呼叫java.io.PrintStream物件的println例項方法, 列印棧頂的Static Initializing... 16 return //返回
II. 類載入器
1. 類載入器ClassLoader
java.lang.ClassLoader本身是一個抽象類,它的例項用來載入Java類到JVM記憶體中。這裡如果細心的小夥伴就會發現,java.lang.ClassLoader的例項用來載入Java類,但是它本身也是一個Java類,誰來載入它?先有雞,還是先有蛋?? 不急,待我們細細說來!! 首先,我們看一個簡單的示例,看看都有哪些不同的類載入器:public static void printClassLoader() { // StringOP:自定義類 System.out.println("ClassLoader of StringOp: " + StringOp.class.getClassLoader()); // com.sun.javafx.binding.Logging:Java核心類擴充套件的類 System.out.println("ClassLoader of Logging: " + Logging.class.getClassLoader()); // java.lang.String: Java核心類 System.out.println("ClassLoader of String: " + String.class.getClassLoader()); }
output:
ClassLoader of StringOp: sun.misc.Launcher$AppClassLoader@18b4aac2 ClassLoader of Logging: sun.misc.Launcher$ExtClassLoader@7c3df479 ClassLoader of String: null從輸出可以看出,這裡有三種不同的類載入器:應用類載入器(Application/System class loader), 擴充套件類載入器(Extension class loader)以及啟動類載入器(Bootstrap class loader)。
- 啟動類載入器:原生代碼(C++語言)實現的類載入器,負責載入JDK內部類(通常是$JAVA_HOME/jre/lib/rt.jar和$JAVA_HOME/jre/lib目錄中的其他核心類庫)或者-Xbootclasspath選項指定的jar包到記憶體中。該載入器是JVM核心的一部分,以本機程式碼編寫,開發者無法獲得啟動類載入器的引用,所以上述java.lang.String類的載入為null。此外,該類充當所有其他java.lang.Class Loader例項共同的父級(區別為是否為直接父級),它載入所有直接子級的java.lang.ClassLoader類(其他子類逐層由直接父級類載入器載入)。
- 擴充套件類載入器:啟動類載入器的子級,由Java語言實現的,用來載入JDK擴充套件目錄下核心類的擴充套件類(通常是$JAVA_HOME/lib/ext/*.jar)或者-Djava.ext.dir系統屬性中指定的任何其他目錄中存在的類到記憶體中。由sun.misc.Launcher$ExtClassLoader類實現,開發者可以直接使用擴充套件類載入器。
- 應用/系統類載入器:擴充套件類載入器的子級,負責將java -classpath/-cp($CLASSPATH)或者-Djava.class.path變數指定目錄下類庫載入到JVM記憶體中。由sun.misc.Launcher$AppClassLoader類實現,開發者可以直接使用系統類載入器。
2. 類載入器的類圖關係
通過上文的分析,目前常用的三種類載入器分別為:啟動類載入器,擴充套件類載入器以及應用/系統載入器。但是檢視原始碼的類圖關係,可以發現AppClassLoder和ExtClassLoader都是sun.misc.Laucher(主要被系統用於啟動主應用程式)這個類的靜態內部類,並且兩個類之間也不存在繼承關係,那為何說應用/系統類載入器是擴充套件類載入器的子級呢? 原始碼分析(JDK1.8): sun.misc.Laucher Launcher.ExtClassLoader.getExtClassLoader():獲取ExtClassLoader例項物件。 Launcher.AppClassLoader.getAppClassLoader(final ClassLoader var0): 根據ExtClassLoader例項物件獲取AppClassLoader例項物件。 Launcher.AppClassLoader(URL[] var1, ClassLoader var2): 根據$CLASSPATH和ExtClassLoader例項物件建立AppClassLoader例項物件。 層層剖析,可見雖然AppClassLoader類和ExtClassLoader類雖然並無繼承(父子)關係,但是在建立AppClassLoader類的例項物件時,顯式(this.parent=parent)設定其父級為ExtClassLoader例項物件,所以雖然從類本身來說兩者並無繼承關係,但例項化出來的物件卻存在父子關係。 一般而言,在Java的日常開發中,通常是由上述三種類載入器相互配合完成的,當然,也可以使用自定義類載入器。需要注意的是,這裡的JVM對.class檔案是按需載入的或者說是Lazy模式,當需要使用某個類時才會將該.class載入到記憶體中生成java.lang.Class物件,並且每個.class檔案只會生成一個java.lang.Class物件。 但幾種載入器時如何配合的呢?亦或是單槍匹馬,各領風騷? 鑑於此,則不得不提JVM採用的雙親委派機制了。3. 雙親委派機制
核心思想:自底向上檢查類是否已載入,自頂向下嘗試載入類。 使用雙親委派模式的優勢:- 使用雙親委派模式可以避免類的重複載入:當父級載入器已經載入了目標類,則子載入器沒有必要再載入一次。
- 避免潛在的安全風險:啟動類載入器是所有其他載入器的共同父級,所以java的核心類庫不會被重複載入,意味著核心類庫不會被隨意篡改。例如我們自定義名為java.lang.String的類,通過雙親委派模式進行載入類,通過上述流程圖,啟動類載入器會發現目標類已經載入,直接返回核心類java.lang.String,而不會通過應用/系統類載入器載入自定義類java.lang.String。當然,一般而言我們是不可以載入全侷限定名與核心類同名的自定義類,否則會丟擲異常:java.lang.SecurityException: Prohibited package name: java.lang。
- 當載入器收到載入類的請求時,首先會根據該類的全侷限定名查目標類是否已經被載入,如果載入則萬事大吉;
- 如果沒有載入,檢視是否有父級載入器,如果有則將載入類的請求委託給父級載入器;
- 依次遞迴;
- 直到啟動類載入器,如果在已載入的類中依舊找不到該類,則由啟動類載入器開始嘗試從所負責的目錄下尋找目標類,如果找到則載入到JVM記憶體中;
- 如果找不到,則傳輸到子級載入器,從負責的目錄下尋找並載入目標類;
- 依次遞迴;
- 直到請求的類載入器依舊找不到,則丟擲java.lang.ClassNotFoundException異常。
作者:吳家二少 部落格地址:https://www.cnblogs.com/cloudman-open/ 本文歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線