本篇內容包括:Java 類的加載機制(Jvm 結構組成、Java 類的加載)、類的生命周期(加載-驗證-準備-解析-初始化-使用-卸載)、類加載器 以及 雙親委派模型。
一、Java 類的加載機制
1、 Jvm 結構組成
Jvm 整體組成可分為四個部分:類加載器、運行時數據區(Runtime Data Area)、執行引擎(Execution Engine)、本地庫接口(Native Interface)
- 類加載器:負責從字節碼(Class)文件中,加載 class 信息到運行時數據區的方法區;
- 運行時數據區:存放 Jvm 在執行 Java 程序時相關數據的區域;
- 執行引擎:將字節碼翻譯成底層系統指令再交由 CPU 去執行;
- 本地庫接口:執行過程中可能需要調用到其他語言(比如 C 語言)的本地接口。
PS:Javac 是收錄于 Jdk 中的 Java 語言編譯器。該工具可以將后綴名為 .java 的源文件編譯為后綴名為 .class 的可以運行于 Java 虛擬機的字節碼。
程序在被執行之前, Java 代碼會被先轉換成字節碼(.class 文件), Jvm 首先通過一定的方式類加載器①(ClassLoader)把字節碼文件加載到內存中運行時數據區②(Runtime Data Area),而字節碼文件是 Jvm 提供的一套指令集規范,并不能直接交個底層操作系統去執行,因此需要特定的命令解析器執行引擎③(Execution Engine)將字節碼翻譯成底層系統指令再交由 CPU 去執行,而這個過程中需要調用其他語言的接口本地庫接口④(Native Interface)來實現整個程序的功能,這就是這 4 個主要組成部分的職責與功能。

而我們通常所說的 Jvm 組成指的是運行時數據區,因為通常需要程序員調試分析的區域就是運行時數據區,或者更具體的來說就是運行時數據區里面的堆(Heap)模塊!
2、Java 類的加載
類的加載指的是將類的 .class 文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然后在堆區創建一個 java.lang.Class 對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位于堆區中的 Class 對象,Class 對象封裝了類在方法區內的數據結構,并且向 Java 程序員提供了訪問方法區內的數據結構的接口。
類加載器并不需要等到某個類被首次主動使用時再加載它, Jvm 規范允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了 .class 文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤( LinkageError 錯誤)如果這個類一直沒有被程序主動使用,那么類加載器就不會報告錯誤。
加載.class 文件的方式:
- 從本地系統中直接加載
- 通過網絡下載 .class 文件
- 從 zip、jar 等歸檔文件中加載 .class 文件
- 從專有數據庫中提取 .class 文件
- 將 Java 源文件動態編譯為 .class 文件
- 由其他文件生成
二、Java 類的生命周期
類從被加載到 Jvm 內存中開始到卸載出內存為止,生命周期分為7個階段:加載-驗證-準備-解析-初始化-使用-卸載。(或分為5個階段,把 驗證-準備-解析 分為連接階段)

1、加載
加載過程就是把 class 字節碼文件載入到虛擬機中,至于從哪兒加載,虛擬機設計者并沒有限定,你可以從文件、壓縮包、網絡、數據庫等等地方加載 class 字節碼。
- 通過一個類的全限定名來獲取其定義的二進制字節流;
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;
- 在Java堆中生成一個代表這個類的 java.lang.Class 對象,作為對方法區中這些數據的訪問入口
2、驗證(連接階段的第一步):確保被加載的類的正確性
這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反復驗證,那么可以考慮采用 -Xverifynone 參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
3、準備(連接階段的第二步):為類的靜態變量分配內存,并將其初始化為默認值
準備階段的工作就是為類的靜態變量分配內存并設為 Jvm 默認的初值,對于非靜態的變量,則不會為它們分配內存。靜態變量的初值為 Jvm 默認的初值,而不是我們在程序中設定的初值。(僅包含類變,不包含實例變量)
4、解析(連接階段的第三步):把類中的符號引用轉換為直接引用
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
5、初始化:為類的靜態變量賦予正確的初始值
主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:
- 聲明類變量是指定初始值;
- 使用靜態代碼塊為類變量指定初始值
Jvm初始化步驟:
-
假如這個類還沒有被加載和連接,則程序先加載并連接該類
-
假如該類的直接父類還沒有被初始化,則先初始化其直接父類
-
假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化時機:只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:
- 創建類的實例,也就是new的方式
- 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
- 調用類的靜態方法
- 反射(如 Class.forName(“com.shengsiyuan.Test”) )
- 初始化某個類的子類,則其父類也會被初始化
- Java虛擬機啟動時被標明為啟動類的類( JavaTest ),直接使用 java.exe 命令來運行某個主類
三、類加載器
類加載器是負責將可能是網絡上、也可能是磁盤上的 .class 文件加載到內存中。并為其生成對應的 java.lang.Class 對象。一旦一個類被載入 Jvm 了,同一個類就不會被再次加載。那么怎樣才算是同一個類?在 Java 中一個類用其全限定類名(包名和類名)作為其唯一標識,但是在 Jvm 中,一個類用其全限定類名和其類加載器作為其唯一標識。也就是說,在 Java 中的同一個類,如果用不同的類加載器加載,則生成的 .class 對象認為是不同的。
當 Jvm啟動時,會形成由三個類加載器組成的初始類加載器層次結構:
- 啟動類加載器(Bootstrap ClassLoader):是嵌在 Jvm 內核中的加載器,該加載器是用 C++ 語言寫的,主要負載加載 JAVA_HOME/lib 下的類庫,啟動類加載器無法被應用程序直接使用;
- 擴展類加載器(Extension ClassLoader):該加載器器是用JAVA編寫,且它的父類加載器是 Bootstrap,是由
sun.misc.Launcher$ExtClassLoader
實現的,主要加載 JAVA_HOME/lib/ext 目錄中的類庫。開發者可以這幾使用擴展類加載器; - 系統類加載器(App ClassLoader):也稱為應用程序類加載器,負責加載應用程序 classpath 目錄下的所有 .jar 和 .class 文件。它的父加載器為 Extension ClassLoader。
上述三種類加載器的層次關系如下:
Ps:類加載器的體系并不是“繼承”體系,而是委派體系,大多數類加載器首先會到自己的 parent 中查找類或者資源,如果找不到才會到自己本地查找。類加載器的委托行為動機是為了避免相同的類被加載多次。
五、雙親委派模型
1、雙親委派模型
如果以上三種類加載器不能滿足要求的話,程序員還可以自定義類加載器(繼承 java.lang.ClassLoader 類)
它們的層級關系即 自定義類加載器 -> 應用程序加載器 -> 擴展加載器 -> 啟動類加載器,這種層次關系被稱作為雙親委派模型:如果一個類加載器收到了加載類的請求,它會先把請求委托給上層加載器去完成,上層加載器又會委托上上層加載器,一直到最頂層的類加載器;如果上層加載器無法完成類的加載工作時,當前類加載器才會嘗試自己去加載這個類。
2、雙親委派模式優勢
- 采用雙親委派模式的是好處是 Java 類隨著它的類加載器一起具備了一種帶有先級的層次關系,通過這種層級關可以避免類的重復加載,當父親已經加載了該類時,就沒有必要子 ClassLoader 再加載一次;
- 其次是考慮到安全因素,Java 核心 api 中定義類型不會被隨意替換,假設通過網絡傳遞一個名為 java.lang.Integer 的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心 Java API 發現這個名字的類,發現該類已被加載,并不會重新加載網絡傳遞的過來的 java.lang.Integer,而直接返回已加載過的 Integer.class,這樣便可以防止核心 API 庫被隨意篡改;
- 可能你會想,如果我們在 classpath 路徑下自定義一個名為 java.lang.SingleInterge 類呢?該類并不存在 java.lang 中,經過雙親委托模式,傳遞到啟動類加載器中,由于父類加載器路徑下并沒有該類,所以不會加載,將反向委托給子類加載器加載,最終會通過系統類加載器加載該類。但是這樣做是不允許,因為 java.lang 是核心 API 包,需要訪問權限,強制加載將會報出異常
java.lang.SecurityException: Prohibited package name: java.lang
所以無論如何都無法加載成功的。