什么是類加載
每個編寫的".java"拓展名類文件都存儲著需要執行的程序邏輯,這些".java"文件經過Java編譯器編譯成拓展名為".class"的文件,".class"文件中保存著Java代碼經轉換后的虛擬機指令,當需要使用某個類時,虛擬機將會加載它的".class"文件,并創建對應的class對象,將class文件加載到虛擬機的內存,這個過程稱為類加載。
類加載的生命周期

如圖所示,JVM類加載機制分為五個部分:加載,驗證,準備,解析,初始化。
加載
加載階段是類加載過程的第一個階段。在這個階段,JVM 的主要目的是將字節碼從各個位置(網絡、磁盤等)轉化為二進制字節流加載到內存中,接著會為這個類在 JVM 的方法區創建一個對應的 Class 對象,這個 Class 對象就是這個類各種數據的訪問入口。
驗證
驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:
- 文件格式驗證:驗證字節流是否符合Class文件格式的規范;例如:是否以
0xCAFEBABE
開頭、主次版本號是否在當前虛擬機的處理范圍之內、常量池中的常量是否有不被支持的類型。 - 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規范的要求;例如:這個類是否有父類,除了
java.lang.Object
之外。 - 字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
- 符號引用驗證:確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反復驗證,那么可以考慮采用-Xverifynone
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
準備(重點)
當完成字節碼文件的校驗之后,JVM 便會開始為類變量分配內存并初始化。這里需要注意兩個關鍵點,即內存分配的對象以及初始化的類型。
- 內存分配的對象。Java 中的變量有「類變量」和「類成員變量」兩種類型,「類變量」指的是被 static 修飾的變量,而其他所有類型的變量都屬于「類成員變量」。在準備階段,JVM 只會為「類變量」分配內存,而不會為「類成員變量」分配內存。「類成員變量」的內存分配需要等到初始化階段才開始。
例如下面的代碼在準備階段,只會為 factor 屬性分配內存,而不會為 website 屬性分配內存。
public static int factor = 3;
public String website = "www.baidu.com";
- 初始化的類型。在準備階段,JVM 會為類變量分配內存,并為其初始化。但是這里的初始化指的是為變量賦予 Java 語言中該數據類型的零值,而不是用戶代碼里初始化的值。
例如下面的代碼在準備階段之后,sector 的值將是 0,而不是 3。
public static int sector = 3;
但如果一個變量是常量(被 static final 修飾)的話,那么在準備階段,屬性便會被賦予用戶希望的值。例如下面的代碼在準備階段之后,number 的值將是 3,而不是 0。
public static final int number = 3;
兩個語句的區別是一個有 final 關鍵字修飾,另外一個沒有。而 final 關鍵字在 Java 中代表不可改變的意思,number 的值一旦賦值就不會在改變了。既然一旦賦值就不會再改變,那么就必須一開始就給其賦予用戶想要的值,因此被 final 修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final 修飾的類變量,其可能在初始化階段或者運行階段發生變化,所以就沒有必要在準備階段對它賦予用戶想要的值。
解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
- 符號引用:簡單的理解就是字符串,比如引用一個類,java.util.ArrayList 這就是一個符號引用,字符串引用的對象不一定被加載。
- 直接引用:指針或者地址偏移量。引用對象一定在內存(已經加載)。
初始化
初始化,這個階段就是執行類構造器< clinit >()方法的過程,為類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:
- ①聲明類變量是指定初始值
- ②使用靜態代碼塊為類變量指定初始值
JVM初始化步驟
- 1、假如這個類還沒有被加載和連接,則程序先加載并連接該類
- 2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
- 3、假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化時機:只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:
- 創建類的實例,也就是new的方式
- 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
- 調用類的靜態方法
- 反射(如
Class.forName(“com.shengsiyuan.Test”)
) - 初始化某個類的子類,則其父類也會被初始化
- Java虛擬機啟動時被標明為啟動類的類(
Java Test
),直接使用java.exe
命令來運行某個主類
使用
當 JVM 完成初始化階段之后,JVM 便開始從入口方法開始執行用戶的程序代碼。
卸載
當用戶程序代碼執行完畢后,JVM 便開始銷毀創建的 Class 對象,最后負責運行的 JVM 也退出內存。
類加載器
顧名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。JVM提供了3種類加載器:

啟動類加載器:Bootstrap ClassLoader
,負責加載存放在JDKjrelib
(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath
參數指定的路徑中的,并且能被虛擬機識別的類庫(如rt.jar,所有的java.開頭的類均被Bootstrap ClassLoader
加載)。啟動類加載器是無法被Java程序直接引用的。擴展類加載器:Extension ClassLoader
,該加載器由sun.misc.Launcher$ExtClassLoader
實現,它負責加載JDKjrelibext
目錄中,或者由java.ext.dirs
系統變量指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴展類加載器。應用程序類加載器:Application ClassLoader
,該類加載器由sun.misc.Launcher$AppClassLoader
來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。
雙親委派模型
下圖中展示了類加載器直接的關系和雙親委派模型

從圖中我們發現除啟動類加載器外,每個加載器都有父的類加載器。
雙親委派機制:如果一個類加載器在接到加載類的請求時,它首先不會自己嘗試去加載這個類,而是把這個請求任務委托給父類加載器去完成,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。

從類的繼承關系來看,ExtClassLoader和AppClassLoader都是繼承URLClassLoader,都是ClassLoader的子類。而BootStrapClassLoader是有C寫的,不再java的ClassLoader子類中。
從圖中可以看到類加載器間的父子關系不是以繼承的方式實現的,而是以組合關系的方式來復用父加載器的代碼。如果一個類加載器收到了類加載的請求,它首先會把這個請求委派給父加載器去完成,每一個層次的類加載器都是如此。 雙親委派模型的好處
Java類隨著加載它的類加載器一起具備了一種帶有優先級的層次關系。比如,Java中的Object類,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object在各種類加載環境中都是同一個類。如果不采用雙親委派模型,那么由各個類加載器自己取加載的話,那么系統中會存在多種不同的Object類。
jvm類加載相關鏈接:
https://blog.csdn.net/javazejian/article/details/73413292
https://www.jianshu.com/p/3cab74a189de
https://www.fangzhipeng.com/javainterview/2019/04/15/class-loader.html
http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html
https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html