前言
我們知道, 在java開發(fā)中, .java文件會被編譯超成一個(gè)個(gè).class文件, 最終被JVM加載和運(yùn)行.
大致流程圖如下

什么是類的加載
我們寫的java文件保存著業(yè)務(wù)邏輯代碼,
java編譯器負(fù)責(zé)將 .java 文件編譯成 .class 文件,
.class 文件中保存著java文件轉(zhuǎn)換后虛擬機(jī)將要執(zhí)行的指令.
當(dāng)需要某個(gè)類的時(shí)候, java虛擬機(jī)會加載 .class 文件,并創(chuàng)建對應(yīng)的class對象.
將class文件加載到虛擬機(jī)的內(nèi)存, 這個(gè)過程被稱為類的加載.
類加載的最終產(chǎn)品是位于堆區(qū)中的Class對象,
Class對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu), 并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口.
類加載器并不需要等到某個(gè)類被“首次主動使用”時(shí)再加載它, JVM規(guī)范允許類加載器在預(yù)料某個(gè)類將要被使用時(shí)就預(yù)先加載它.
如果在預(yù)先加載的過程中遇到了.class文件缺失或存在錯(cuò)誤,類加載器必須在程序首次主動使用該類時(shí)才報(bào)告錯(cuò)誤(LinkageError錯(cuò)誤).
如果這個(gè)類一直沒有被程序主動使用,那么類加載器就不會報(bào)告錯(cuò)誤.
知道了什么類的加載, 下面我們就來了解下JAVA類的生命周期
JAVA類的生命周期

可以看出, JAVA類的生命周期如下:
- 加載
- 驗(yàn)證
- 準(zhǔn)備
- 解析
- 初始化
- 使用
- 卸載
類加載的過程包括了加載、驗(yàn)證、準(zhǔn)備、解析、初始化五個(gè)階段.
PS:
驗(yàn)證、準(zhǔn)備、解析這三個(gè)階段也被統(tǒng)稱為連接階段
在這五個(gè)階段中,加載、驗(yàn)證、準(zhǔn)備和初始化這四個(gè)階段發(fā)生的順序是確定的;
而解析階段則不一定,它在某些情況下可以在初始化階段之后開始,這是為了支持Java語言的運(yùn)行時(shí)綁定(也成為動態(tài)綁定或晚期綁定).
PS:
另外注意這里的幾個(gè)階段是按順序開始,而不是按順序進(jìn)行或完成,因?yàn)檫@些階段通常都是互相交叉地混合進(jìn)行的,通常在一個(gè)階段執(zhí)行的過程中調(diào)用或激活另一個(gè)階段.
加載
加載階段主要查找并加載類的二進(jìn)制數(shù)據(jù), 類加載器通過一個(gè)類的完全限定名查找此類字節(jié)碼文件,并利用字節(jié)碼文件創(chuàng)建一個(gè)class對象.
在加載階段, 虛擬機(jī)需要完成以下三件事情:
- 通過一個(gè)類的全限定名來獲取其定義的二進(jìn)制字節(jié)流
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對象,作為對方法區(qū)中這些數(shù)據(jù)的訪問入口
相對于類加載的其他階段而言,加載階段是可控性最強(qiáng)的階段, 因?yàn)殚_發(fā)人員既可以使用系統(tǒng)提供的類加載器來完成加載, 也可以使用自己定義的類加載器來完成加載.
JVM類加載器
類的加載由類加載器完成,類加載器通常由JVM提供.
JVM提供的這些類加載器通常被稱為系統(tǒng)類加載器;除此之外,開發(fā)者可以通過繼承ClassLoader基類來創(chuàng)建自己的類加載器.
JVM預(yù)定義有三種類加載器:
- 啟動類加載器(Bootstrap ClassLoader)
用來加載 Java 的核心類,是用原生代碼來實(shí)現(xiàn)的,并不繼承自java.lang.ClassLoader(負(fù)責(zé)加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實(shí)現(xiàn),不是ClassLoader子類);
由于引導(dǎo)類加載器涉及到虛擬機(jī)本地實(shí)現(xiàn)細(xì)節(jié),開發(fā)者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進(jìn)行操作
//獲得根類加載器所加載的核心類庫,并會看到本機(jī)安裝的Java環(huán)境變量指定的jdk中提供的核心jar包路徑
public class ClassLoaderTest {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls){
System.out.println(url.toExternalForm());
}
}
}
結(jié)果
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/classes
擴(kuò)展類加載器(Extension ClassLoader)
它負(fù)責(zé)加載JRE的擴(kuò)展目錄,lib/ext或者由java.ext.dirs系統(tǒng)屬性指定的目錄中的JAR包的類, 由Java語言實(shí)現(xiàn),父類加載器為null應(yīng)用程序類加載器(Application ClassLoader)
被稱為系統(tǒng)(也稱為應(yīng)用)類加載器,它負(fù)責(zé)在JVM啟動時(shí)加載來自Java命令的-classpath選項(xiàng)、java.class.path系統(tǒng)屬性,或者CLASSPATH換將變量所指定的JAR包和類路徑.
程序可以通過ClassLoader的靜態(tài)方法getSystemClassLoader()來獲取系統(tǒng)類加載器;如果沒有特別指定,則用戶自定義的類加載器都以此類加載器作為父加載器,由Java語言實(shí)現(xiàn),父類加載器為ExtClassLoader.
類加載器加載Class大致要經(jīng)過如下8個(gè)步驟:
- 檢測此Class是否載入過,即在緩沖區(qū)中是否有此Class,如果有直接進(jìn)入第8步,否則進(jìn)入第2步
- 如果沒有父類加載器,則要么Parent是根類加載器,要么本身就是根類加載器,則跳到第4步,如果父類加載器存在,則進(jìn)入第3步
- 請求使用父類加載器去載入目標(biāo)類,如果載入成功則跳至第8步,否則接著執(zhí)行第5步
- 請求使用根類加載器去載入目標(biāo)類,如果載入成功則跳至第8步,否則跳至第7步
- 當(dāng)前類加載器嘗試尋找Class文件,如果找到則執(zhí)行第6步,如果找不到則執(zhí)行第7步
- 從文件中載入Class,成功后跳至第8步
- 拋出ClassNotFountException異常
- 返回對應(yīng)的java.lang.Class對象
JVM類加載機(jī)制
JVM的類加載機(jī)制主要有如下3種:
全盤負(fù)責(zé)
所謂全盤負(fù)責(zé),就是當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)Class時(shí),該Class所依賴和引用其他Class也將由該類加載器負(fù)責(zé)載入,除非顯示使用另外一個(gè)類加載器來載入緩存機(jī)制
緩存機(jī)制將會保證所有加載過的Class都會被緩存,當(dāng)程序中需要使用某個(gè)Class時(shí),類加載器先從緩存區(qū)中搜尋該Class,只有當(dāng)緩存區(qū)中不存在該Class對象時(shí),系統(tǒng)才會讀取該類對應(yīng)的二進(jìn)制數(shù)據(jù),并將其轉(zhuǎn)換成Class對象,存入緩沖區(qū)中.這就是為什么修改了Class后,必須重新啟動JVM,程序所做的修改才會生效的原因雙親委派
雙親委派就是如果一個(gè)類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個(gè)類,而是把請求委托給父加載器去完成, 若成功則直接返回, 否則繼續(xù)向上,直到到達(dá)最頂層的類加載器;
因此,所有的類加載請求最終都應(yīng)該被傳遞到頂層的啟動類加載器中,只有當(dāng)父加載器無法完成該加載請求時(shí),子加載器才會嘗試自己去加載該類

JVM 通過雙親委派模型進(jìn)行類的加載,當(dāng)然我們也可以通過繼承java.lang.ClassLoader實(shí)現(xiàn)自定義的類加載器.
采用雙親委派的一個(gè)好處是比如加載位于 rt.jar 包中的java.lang.Object,不管是哪個(gè)加載器加載這個(gè)類,最終都是委托給頂層的啟動類加載器進(jìn)行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個(gè) Object 對象
簡而言之:
- 系統(tǒng)類防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼
- 保證Java程序安全穩(wěn)定運(yùn)行
JVM類加載方式
JVM有3種類加載方式:
命令行啟動應(yīng)用時(shí)候由JVM初始化加載
通過ClassLoader.loadClass()方法動態(tài)加載
將.class文件加載到j(luò)vm中,不會執(zhí)行static中的內(nèi)容,只有在newInstance才會去執(zhí)行static塊;
Classloader.loaderClass得到的class是還沒有連接(驗(yàn)證、準(zhǔn)備、解析)的通過Class.forName()方法動態(tài)加載
將類的.class文件加載到j(luò)vm中,還會對類進(jìn)行解釋,執(zhí)行類中的static塊; Class.forName()得到的class是已經(jīng)初始化完成的
實(shí)例:
package com.test.classloader;
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()來加載類,不會執(zhí)行初始化塊
loader.loadClass("TestClass");
//使用Class.forName()來加載類,默認(rèn)會執(zhí)行初始化塊
Class.forName("TestClass");
//使用Class.forName()來加載類,并指定ClassLoader,初始化時(shí)不執(zhí)行靜態(tài)塊
Class.forName("TestClass", false, loader);
}
}
public class Test {
static {
System.out.println("靜態(tài)初始化塊執(zhí)行了!");
}
}
驗(yàn)證
驗(yàn)證階段是為了確保 Class 文件的字節(jié)流中包含的信息是符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全.
驗(yàn)證大致會完成4個(gè)檢驗(yàn)動作:
文件格式驗(yàn)證
驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范.
例如: 是否以0xCAFEBABE開頭; 主次版本號是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi); 常量池中的常量是否有不被支持的類型;元數(shù)據(jù)驗(yàn)證
對字節(jié)碼描述的信息進(jìn)行語義分析(注意: 對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規(guī)范的要求;
例如:這個(gè)類是否有父類, 除了java.lang.Object之外;
字節(jié)碼驗(yàn)證
通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的.符號引用驗(yàn)證
確保解析動作能正確執(zhí)行.
驗(yàn)證階段是非常重要的,但不是必須的,它對程序運(yùn)行期沒有影響;
如果所引用的類經(jīng)過反復(fù)驗(yàn)證,那么可以考慮采用-Xverifynone參數(shù)來關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間.
準(zhǔn)備
準(zhǔn)備階段是為static修飾的類變量分配內(nèi)存, 并設(shè)置類變量初始值的階段;
這些內(nèi)存都將在方法區(qū)中分配; 不包含final修飾的靜態(tài)變量, 因?yàn)閒inal變量在編譯時(shí)分配.需要注意的是:
- 此時(shí)進(jìn)行內(nèi)存分配的僅包括類變量(static),而不包括實(shí)例變量; 實(shí)例變量會在對象實(shí)例化時(shí)隨著對象一塊分配在Java堆中.
- 這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類型默認(rèn)的零值(如0、0L、null、false等)而不是被在Java代碼中被顯式地賦予的值.
舉個(gè)例子:
類中定義了變量public static int a =100;
實(shí)際上變量 a 在準(zhǔn)備階段過后的初始值為 0 而不是100;(對這句話有疑惑的可以去補(bǔ)充下JAVA數(shù)據(jù)類型初始值的知識)
將 a 賦值為 100 的put static指令是程序被編譯后, 存放于類構(gòu)造器<clinit>()方法之中的.不過注意,如果聲明為:
public static final int a = 100;
在編譯階段會為 a 生成 ConstantValue 屬性, 在準(zhǔn)備階段虛擬機(jī)會根據(jù) ConstantValue 屬性將 a 賦值為 100
解析
解析階段是指虛擬機(jī)將常量池中的
符號引用替換為直接引用的過程,
主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號引用進(jìn)行.
符號引用就是一組符號來描述目標(biāo),可以是任何字面量.直接引用
直接引用可以是指向目標(biāo)的指針,相對偏移量或是一個(gè)能間接定位到目標(biāo)的句柄; 如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在.
符號引用
符號引用與虛擬機(jī)實(shí)現(xiàn)的布局無關(guān),引用的目標(biāo)并不一定要已經(jīng)加載到內(nèi)存中.
各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號引用必須是一致的; 因?yàn)榉栆玫淖置媪啃问矫鞔_定義在 Java 虛擬機(jī)規(guī)范的 Class 文件格式中.
初始化
這里是類記載的最后階段,如果該類具有父類就進(jìn)行對父類進(jìn)行初始化,執(zhí)行其靜態(tài)初始化器(靜態(tài)代碼塊)和靜態(tài)初始化成員變量.(前面已經(jīng)對static 初始化了默認(rèn)值,這里我們對它進(jìn)行賦值,成員變量也將被初始化).
JVM負(fù)責(zé)對類進(jìn)行初始化,主要對類變量進(jìn)行初始化.
Java對類變量進(jìn)行初始值設(shè)定的兩種方式:
- 聲明類變量是指定初始值
- 使用靜態(tài)代碼塊為類變量指定初始值
JVM初始化步驟
- 假如這個(gè)類還沒有被加載和連接,則程序先加載并連接該類
- 假如該類的直接父類還沒有被初始化,則先初始化其直接父類
- 假如類中有初始化語句,則系統(tǒng)依次執(zhí)行這些初始化語句
類初始化時(shí)機(jī)
只有當(dāng)對類的主動使用的時(shí)候才會導(dǎo)致類的初始化,類的主動使用包括以下六種:
- 創(chuàng)建類的實(shí)例,也就是new的方式
- 訪問某個(gè)類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
- 調(diào)用類的靜態(tài)方法
- 反射(如Class.forName(“com.test.Test”))
- 初始化某個(gè)類的子類,則其父類也會被初始化
- Java虛擬機(jī)啟動時(shí)被標(biāo)明為啟動類的類(Java Test),直接使用java.exe命令來運(yùn)行某個(gè)主類
注意以下幾種情況不會執(zhí)行類初始化:
- 通過子類引用父類的靜態(tài)字段,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化
- 定義對象數(shù)組,不會觸發(fā)該類的初始化
- 常量在編譯期間會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會觸發(fā)定義常量所在的類
- 通過類名獲取 Class 對象,不會觸發(fā)類的初始化
- 通過 Class.forName 加載指定類時(shí),如果指定參數(shù) initialize 為 false 時(shí),也不會觸發(fā)類初始化,其實(shí)這個(gè)參數(shù)是告訴虛擬機(jī),是否要對類進(jìn)行初始化
- 通過 ClassLoader 默認(rèn)的 loadClass 方法,也不會觸發(fā)初始化動作
請關(guān)注我的訂閱號
