筆記來源:尚矽谷JVM全套教程,百萬播放,全網巔峰(宋紅康詳解java虛擬機器)
同步更新:https://gitee.com/vectorx/NOTE_JVM
2. 類載入子系統
2.1. 記憶體結構概述
- Class檔案
- 類載入子系統
- 執行時資料區
- 方法區
- 堆
- 程式計數器
- 虛擬機器棧
- 本地方法棧
- 執行引擎
- 本地方法介面
- 本地方法庫
如果自己想手寫一個Java虛擬機器的話,主要考慮哪些結構呢?
- 類載入器
- 執行引擎
2.2. 類載入器與類的載入過程
類載入器子系統作用
- 類載入器子系統負責從檔案系統或者網路中載入Class檔案,class檔案在檔案開頭有特定的檔案標識。
- ClassLoader只負責class檔案的載入,至於它是否可以執行,則由Execution Engine決定。
- 載入的類資訊存放於一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中還會存放執行時常量池資訊,可能還包括字串字面量和數字常量(這部分常量資訊是Class檔案中常量池部分的記憶體對映)
類載入器ClasLoader角色
- class file存在於本地硬碟上,可以理解為設計師畫在紙上的模板,而最終這個模板在執行的時候是要載入到JVM當中來根據這個檔案例項化出n個一模一樣的例項。
- class file載入到JVM中,被稱為DNA後設資料模板,放在方法區。
- 在.class檔案->JVM->最終成為後設資料模板,此過程就要一個運輸工具(類裝載器Class Loader),扮演一個快遞員的角色。
類的載入過程
/**
*示例程式碼
*/
public class HelloLoader {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
用流程圖表示上述示例程式碼:
載入階段
-
- 通過一個類的全限定名獲取定義此類的二進位制位元組流
-
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
-
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口
補充:載入class檔案的方式
- 從本地系統中直接載入
- 通過網路獲取,典型場景:Web Applet
- 從zip壓縮包中讀取,成為日後jar、war格式的基礎
- 執行時計算生成,使用最多的是:動態代理技術
- 由其他檔案生成,典型場景:JSP應用
- 從專有資料庫中提取.class檔案,比較少見
- 從加密檔案中獲取,典型的防Class檔案被反編譯的保護措施
連結階段
- 驗證(Verify):
- 目的在子確保Class檔案的位元組流中包含資訊符合當前虛擬機器要求,保證被載入類的正確性,不會危害虛擬機器自身安全。
- 主要包括四種驗證,檔案格式驗證,後設資料驗證,位元組碼驗證,符號引用驗證。
- 準備(Prepare):
- 為類變數分配記憶體並且設定該類變數的預設初始值,即零值。
- 這裡不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯式初始化;
- 這裡不會為例項變數分配初始化,類變數會分配在方法區中,而例項變數是會隨著物件一起分配到Java堆中。
- 解析(Resolve):
- 將常量池內的符號引用轉換為直接引用的過程。
- 事實上,解析操作往往會伴隨著JVM在執行完初始化之後再執行。
- 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機器規範》的Class檔案格式中。直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制程式碼。
- 解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別等。對應常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
初始化階段
- 初始化階段就是執行類構造器方法<clinit>()的過程。
- 此方法不需定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來。
- 構造器方法中指令按語句在原始檔中出現的順序執行。
- <clinit>()不同於類的構造器。(關聯:構造器是虛擬機器視角下的<init>())
- 若該類具有父類,JVM會保證子類的<clinit>()執行前,父類的<clinit>()已經執行完畢。
- 虛擬機器必須保證一個類的<clinit>()方法在多執行緒下被同步加鎖。
2.3. 類載入器分類
JVM支援兩種型別的類載入器 。分別為引導類載入器(Bootstrap ClassLoader)和自定義類載入器(User-Defined ClassLoader)。
從概念上來講,自定義類載入器一般指的是程式中由開發人員自定義的一類類載入器,但是Java虛擬機器規範卻沒有這麼定義,而是將所有派生於抽象類ClassLoader的類載入器都劃分為自定義類載入器。
無論類載入器的型別如何劃分,在程式中我們最常見的類載入器始終只有3個,如下所示:
這裡的四者之間的關係是包含關係。不是上層下層,也不是子父類的繼承關係。
2.3.1. 虛擬機器自帶的載入器
啟動類載入器(引導類載入器,Bootstrap ClassLoader)
- 這個類載入使用C/C++語言實現的,巢狀在JVM內部。
- 它用來載入Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身需要的類
- 並不繼承自ava.lang.ClassLoader,沒有父載入器。
- 載入擴充套件類和應用程式類載入器,並指定為他們的父類載入器。
- 出於安全考慮,Bootstrap啟動類載入器只載入包名為java、javax、sun等開頭的類
擴充套件類載入器(Extension ClassLoader)
- Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現。
- 派生於ClassLoader類
- 父類載入器為啟動類載入器
- 從java.ext.dirs系統屬性所指定的目錄中載入類庫,或從JDK的安裝目錄的jre/1ib/ext子目錄(擴充套件目錄)下載入類庫。如果使用者建立的JAR放在此目錄下,也會自動由擴充套件類載入器載入。
應用程式類載入器(系統類載入器,AppClassLoader)
- java語言編寫,由sun.misc.LaunchersAppClassLoader實現
- 派生於ClassLoader類
- 父類載入器為擴充套件類載入器
- 它負責載入環境變數classpath或系統屬性java.class.path指定路徑下的類庫
- 該類載入是程式中預設的類載入器,一般來說,Java應用的類都是由它來完成載入
- 通過ClassLoader#getSystemclassLoader() 方法可以獲取到該類載入器
2.3.2. 使用者自定義類載入器
在Java的日常應用程式開發中,類的載入幾乎是由上述3種類載入器相互配合執行的,在必要時,我們還可以自定義類載入器,來定製類的載入方式。 為什麼要自定義類載入器?
- 隔離載入類
- 修改類載入的方式
- 擴充套件載入源
- 防止原始碼洩漏
使用者自定義類載入器實現步驟:
- 開發人員可以通過繼承抽象類ava.lang.ClassLoader類的方式,實現自己的類載入器,以滿足一些特殊的需求
- 在JDK1.2之前,在自定義類載入器時,總會去繼承ClassLoader類並重寫loadClass() 方法,從而實現自定義的類載入類,但是在JDK1.2之後已不再建議使用者去覆蓋loadclass() 方法,而是建議把自定義的類載入邏輯寫在findClass()方法中
- 在編寫自定義類載入器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass() 方法及其獲取位元組碼流的方式,使自定義類載入器編寫更加簡潔。
2.4. ClassLoader的使用說明
ClassLoader類是一個抽象類,其後所有的類載入器都繼承自ClassLoader(不包括啟動類載入器)
sun.misc.Launcher 它是一個java虛擬機器的入口應用
獲取ClassLoader的途徑
-
方式一:獲取當前ClassLoader
clazz.getClassLoader()
-
方式二:獲取當前執行緒上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
-
方式三:獲取系統的ClassLoader
ClassLoader.getSystemClassLoader()
-
方式四:獲取呼叫者的ClassLoader
DriverManager.getCallerClassLoader()
2.5. 雙親委派機制
Java虛擬機器對class檔案採用的是按需載入的方式,也就是說當需要使用該類時才會將它的class檔案載入到記憶體生成class物件。而且載入某個類的class檔案時,Java虛擬機器採用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式。
工作原理
- 1)如果一個類載入器收到了類載入請求,它並不會自己先去載入,而是把這個請求委託給父類的載入器去執行;
- 2)如果父類載入器還存在其父類載入器,則進一步向上委託,依次遞迴,請求最終將到達頂層的啟動類載入器;
- 3)如果父類載入器可以完成類載入任務,就成功返回,倘若父類載入器無法完成此載入任務,子載入器才會嘗試自己去載入,這就是雙親委派模式。
舉例
當我們載入jdbc.jar 用於實現資料庫連線的時候,首先我們需要知道的是 jdbc.jar是基於SPI介面進行實現的,所以在載入的時候,會進行雙親委派,最終從根載入器中載入 SPI核心類,然後在載入SPI介面類,接著在進行反向委派,通過執行緒上下文類載入器進行實現類jdbc.jar的載入。
優勢
- 避免類的重複載入
- 保護程式安全,防止核心API被隨意篡改
- 自定義類:java.lang.String
- 自定義類:java.lang.ShkStart(報錯:阻止建立 java.lang開頭的類)
沙箱安全機制
自定義String類,但是在載入自定義String類的時候會率先使用引導類載入器載入,而引導類載入器在載入的過程中會先載入jdk自帶的檔案(rt.jar包中java\lang\String.class),報錯資訊說沒有main方法,就是因為載入的是rt.jar包中的string類。這樣可以保證對java核心原始碼的保護,這就是沙箱安全機制。
2.6. 其他
如何判斷兩個class物件是否相同
在JVM中表示兩個class物件是否為同一個類存在兩個必要條件:
- 類的完整類名必須一致,包括包名。
- 載入這個類的ClassLoader(指ClassLoader例項物件)必須相同。
換句話說,在JVM中,即使這兩個類物件(class物件)來源同一個Class檔案,被同一個虛擬機器所載入,但只要載入它們的ClassLoader例項物件不同,那麼這兩個類物件也是不相等的。
對類載入器的引用
JVM必須知道一個型別是由啟動載入器載入的還是由使用者類載入器載入的。如果一個型別是由使用者類載入器載入的,那麼JVM會將這個類載入器的一個引用作為型別資訊的一部分儲存在方法區中。當解析一個型別到另一個型別的引用的時候,JVM需要保證這兩個型別的類載入器是相同的。
類的主動使用和被動使用
Java程式對類的使用方式分為:主動使用和被動使用。
主動使用,又分為七種情況:
-
建立類的例項
-
訪問某個類或介面的靜態變數,或者對該靜態變數賦值
-
呼叫類的靜態方法
-
反射(比如:Class.forName("com.atguigu.Test"))
-
初始化一個類的子類
-
Java虛擬機器啟動時被標明為啟動類的類
-
JDK 7 開始提供的動態語言支援:
java.lang.invoke.MethodHandle例項的解析結果
REF_getStatic、REF_putStatic、REF_invokeStatic控制程式碼對應的類沒有初始化,則初始化
除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化。