博主最近復(fù)習(xí)深入理解JVM一書,整理歸納裙椭,以形成系統(tǒng)認(rèn)識(shí)和方便日后復(fù)習(xí)驴娃。
本文主要介紹
- 類的生命周期
- 類初始化時(shí)機(jī)
- 類加載的過程
一. 類的生命周期
類是在運(yùn)行期間動(dòng)態(tài)加載的。
類的加載指的是將類的.class
文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中岩瘦,將其放在運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi)未巫,然后在堆區(qū)創(chuàng)建一個(gè)java.lang.Class
對(duì)象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)启昧。
類的加載的最終產(chǎn)品是位于堆區(qū)中的Class對(duì)象叙凡,Class對(duì)象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。
類的生命周期
包括以下 7 個(gè)階段:
- 加載(Loading)
- 驗(yàn)證(Verification)
- 準(zhǔn)備(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸載(Unloading)
其中解析過程在某些情況下可以在初始化階段之后再開始,這是為了支持 Java 的動(dòng)態(tài)綁定锌蓄。
二.類初始化時(shí)機(jī)
那么什么情況下需要開始類加載過程的第一個(gè)階段弓坞,加載到內(nèi)存中呢?
- 由主動(dòng)引用觸發(fā),被動(dòng)引用不會(huì)觸發(fā)。
有且僅有5類主動(dòng)引用:
虛擬機(jī)規(guī)范中并沒有強(qiáng)制約束何時(shí)進(jìn)行加載,但是規(guī)范嚴(yán)格規(guī)定了有且只有下列五種情況必須對(duì)類進(jìn)行初始化(加載燥撞、驗(yàn)證、準(zhǔn)備都會(huì)隨著發(fā)生):
1. 特殊字節(jié)碼指令
遇到 new教硫、getstatic叨吮、putstatic、invokestatic 這四條字節(jié)碼指令時(shí)瞬矩,如果類沒有進(jìn)行過初始化茶鉴,則必須先觸發(fā)其初始化。最常見的生成這 4 條指令的場景是:
- 使用 new 關(guān)鍵字實(shí)例化對(duì)象的時(shí)候
- 讀取或設(shè)置一個(gè)類的靜態(tài)字段(被 final 修飾景用、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候
- 調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候
2. 反射調(diào)用
使用 java.lang.reflect
包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候涵叮,如果類沒有進(jìn)行初始化惭蹂,則需要先觸發(fā)其初始化。
3. 父類
當(dāng)初始化一個(gè)類的時(shí)候割粮,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化盾碗,則需要先觸發(fā)其父類的初始化。
4. 主類
當(dāng)虛擬機(jī)啟動(dòng)時(shí)舀瓢,用戶需要指定一個(gè)要執(zhí)行的主類(包含 main() 方法的那個(gè)類)廷雅,虛擬機(jī)會(huì)先初始化這個(gè)主類。
5. 特殊方法句柄
當(dāng)使用 JDK.7 的動(dòng)態(tài)語言支持時(shí)京髓,如果一個(gè) java.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果為 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄航缀,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化堰怨;
以上 5 種場景中的行為稱為對(duì)一個(gè)類進(jìn)行主動(dòng)引用芥玉。除此之外,所有引用類的方式都不會(huì)觸發(fā)初始化备图,稱為被動(dòng)引用灿巧。
被動(dòng)引用的常見例子
1. 通過子類引用父類的靜態(tài)字段
這種情況不會(huì)導(dǎo)致子類的初始化,因?yàn)閷?duì)于靜態(tài)字段揽涮,只有直接定義靜態(tài)字段的類才會(huì)被觸發(fā)初始化抠藕,子類不是定義這個(gè)靜態(tài)字段的類,自然不能被實(shí)例化蒋困。
System.out.println(SubClass.value); // value 字段在 SuperClass 中定義
2. 通過數(shù)組定義來引用類幢痘,不會(huì)觸發(fā)該類的初始化
該過程會(huì)對(duì)數(shù)組類進(jìn)行初始化,數(shù)組類是一個(gè)由虛擬機(jī)自動(dòng)生成的家破、直接繼承自 Object 的子類,其中包含了數(shù)組的屬性和方法购岗。
SuperClass[] sca = new SuperClass[10];
3. 常量不會(huì)觸發(fā)定義常量的類的初始化
因?yàn)槌A吭诰幾g階段會(huì)存入調(diào)用常量的類的常量池中汰聋,本質(zhì)上并沒有引用定義這個(gè)常量的類,所以不會(huì)觸發(fā)定義這個(gè)常量的類的初始化喊积。
System.out.println(ConstClass.HELLOWORLD);
三. 類加載過程
包含了加載烹困、驗(yàn)證、準(zhǔn)備乾吻、解析和初始化這 5 個(gè)階段髓梅。
1. 加載
加載是類加載的一個(gè)階段,不要混淆绎签。
- 加載過程完成以下三件事:
- 通過一個(gè)類的全限定名來
獲取
定義此類的二進(jìn)制字節(jié)流 - 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)
轉(zhuǎn)化
為方法區(qū)的運(yùn)行時(shí)存儲(chǔ)結(jié)構(gòu) - 在內(nèi)存中
生成
一個(gè)代表這個(gè)類的Class 對(duì)象枯饿,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口
相對(duì)于類加載的其他階段而言,加載階段(準(zhǔn)確地說诡必,是加載階段獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是可控性最強(qiáng)的階段奢方,因?yàn)殚_發(fā)人員既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
其中二進(jìn)制字節(jié)流可以從以下方式中獲润帧:
- 從 ZIP 包讀取
這很常見稿蹲,最終成為日后 JAR、EAR鹊奖、WAR 格式的基礎(chǔ)苛聘。 - 從網(wǎng)絡(luò)中獲取
這種場景最典型的應(yīng)用是 Applet。 - 運(yùn)行時(shí)計(jì)算生成
這種場景使用得最多得就是動(dòng)態(tài)代理技術(shù)忠聚,在 java.lang.reflect.Proxy 中设哗,就是用了 ProxyGenerator.generateProxyClass 的代理類的二進(jìn)制字節(jié)流。 - 由其他文件生成
典型場景是 JSP 應(yīng)用咒林,即由 JSP 文件生成對(duì)應(yīng)的 Class 類熬拒。 - 從數(shù)據(jù)庫讀取
這種場景相對(duì)少見,例如有些中間件服務(wù)器(如 SAP Netweaver)可以選擇把程序安裝到數(shù)據(jù)庫中來完成程序代碼在集群間的分發(fā)垫竞。
加載階段完成后澎粟,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,而且在Java堆中也創(chuàng)建一個(gè)java.lang.Class類的對(duì)象欢瞪,這樣便可以通過該對(duì)象訪問方法區(qū)中的這些數(shù)據(jù)活烙。
2. 驗(yàn)證--確保被加載的類的正確性
確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全遣鼓。
驗(yàn)證階段大致會(huì)完成4個(gè)階段的檢驗(yàn)動(dòng)作:
1)文件格式驗(yàn)證
驗(yàn)證字節(jié)流是否符合
Class 文件格式的規(guī)范啸盏,并且能被當(dāng)前版本的虛擬機(jī)處理。
- 是否以魔數(shù)0xCAFEBABE開頭
- 主骑祟、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)回懦。
- 常量池的常量中是否有不被支持的常量類型(檢查常量tag標(biāo)志)。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量次企。
- Class文件中各部分及文件本身是否有被刪除或附加的其他信息
……
2)元數(shù)據(jù)驗(yàn)證
對(duì)字節(jié)碼描述的信息進(jìn)行語義分析怯晕,以保證其描述的信息符合 Java 語言規(guī)范的要求。
1.這個(gè)類是否有父類(除了java.lang.Object之外缸棵,所有的類都應(yīng)當(dāng)有父類)舟茶。
2.這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類);
3.如果這個(gè)類不是抽象類堵第,是否實(shí)現(xiàn)了其父類或接口中要求實(shí)現(xiàn)的所有方法吧凉。
4.類中的字段、方法是否與父類產(chǎn)生矛盾(例如覆蓋了父類的final字段踏志,或者出現(xiàn)不符合規(guī)則的方法重載阀捅。)
……
3)字節(jié)碼驗(yàn)證
通過數(shù)據(jù)流和控制流分析,確保程序語義是合法针余、符合邏輯的也搓。
4)符號(hào)引用驗(yàn)證
發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)換為直接引用的時(shí)候赏廓,對(duì)類自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn)
驗(yàn)證階段是非常重要的,但不是必須的傍妒,它對(duì)程序運(yùn)行期沒有影響幔摸,如果所引用的類經(jīng)過反復(fù)驗(yàn)證,那么可以考慮采用-Xverifynone參數(shù)來關(guān)閉大部分的類驗(yàn)證措施颤练,以縮短虛擬機(jī)類加載的時(shí)間既忆。
3. 準(zhǔn)備--為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值
類變量
是被 static 修飾的變量嗦玖,準(zhǔn)備階段為類變量分配內(nèi)存并設(shè)置初始值患雇,使用的是方法區(qū)的內(nèi)存
- 注意:
-
實(shí)例變量
不會(huì)在這階段分配內(nèi)存,它將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在 Java 堆中宇挫。 - 初始值“通常情況”下是數(shù)據(jù)類型的零值
public static int value = 123;
那變量value在準(zhǔn)備階段過后的初始值為0
而不是123苛吱,因?yàn)檫@時(shí)候尚未開始執(zhí)行任何Java方法,而value賦值為123的putstatic指令是程序被編譯后器瘪,存放于類構(gòu)造器<clinit>方法之中翠储,所以把value賦值為123的動(dòng)作在初始化階段才會(huì)執(zhí)行。
而“特殊情況”橡疼,如:
public static final int value = 123援所;
如果類變量是常量,那么會(huì)按照表達(dá)式來進(jìn)行初始化(例子中的123
)欣除,而不是賦值為 0
4. 解析--把類中的符號(hào)引用轉(zhuǎn)換為直接引用
將常量池的符號(hào)引用替換為直接引用的過程住拭。
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程,解析動(dòng)作主要針對(duì)類或接口历帚、字段滔岳、類方法、接口方法挽牢、方法類型澈蟆、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行。
符號(hào)引用就是一組符號(hào)來描述目標(biāo)卓研,可以是任何字面量。
直接引用就是直接指向目標(biāo)的指針睹簇、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄奏赘。
5. 初始化
初始化階段才真正開始執(zhí)行類中的定義的 Java 程序代碼。初始化階段即虛擬機(jī)執(zhí)行類構(gòu)造器 <clinit>()
方法的過程太惠。
在準(zhǔn)備階段磨淌,類變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段凿渊,根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其它資源梁只。
1. <clinit>()
特點(diǎn)
是由編譯器自動(dòng)收集類中所有類變量的賦值動(dòng)作和靜態(tài)語句塊(static{} 塊)中的語句合并產(chǎn)生的缚柳,編譯器收集的順序由語句在源文件中出現(xiàn)的順序決定。
特別注意的是搪锣,靜態(tài)語句塊只能訪問到定義在它之前的類變量秋忙,定義在它之后的類變量只能賦值,不能訪問
构舟。例如以下代碼:
public class Test {
static {
i = 0; // 給變量賦值可以正常編譯通過
System.out.print(i); // 這句編譯器會(huì)提示“非法向前引用”
}
static int i = 1;
}
2.<clinit>()
超類和派生類
與類的構(gòu)造函數(shù)(或者說實(shí)例構(gòu)造器 <init>())不同灰追,不需要顯式的調(diào)用父類的構(gòu)造器。**虛擬機(jī)會(huì)自動(dòng)保證在子類的<clinit>()
方法運(yùn)行之前狗超,父類
的 <clinit>()
方法已經(jīng)執(zhí)行結(jié)束**弹澎。因此虛擬機(jī)中第一個(gè)執(zhí)行
<clinit>() `方法的類肯定為 java.lang.Object。
由于父類的 <clinit>()
方法先執(zhí)行努咐,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)于子類的變量賦值操作苦蒿。如下面的例子所示,輸出結(jié)果為2而不是1渗稍。
public class Parent {
public static int A = 1;
static{
A = 2;
}
}
public class Sub extends Parent{
public static int B = A;
}
public class Test {
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
3. <clinit>()
不必須
<clinit>()
方法對(duì)于類或接口不是必須的佩迟,如果一個(gè)類中不包含靜態(tài)語句塊,也沒有對(duì)類變量的賦值操作免胃,編譯器可以不為該類生成 <clinit>()
方法音五。
4. 接口的<clinit>()
用到才執(zhí)行
接口中不可以使用靜態(tài)語句塊,但仍然有類變量初始化的賦值操作羔沙,因此接口與類一樣都會(huì)生成 <clinit>() 方法躺涝。但接口與類不同的是,執(zhí)行接口的 <clinit>() 方法不需要先執(zhí)行父接口的 <clinit>() 方法扼雏。只有當(dāng)父接口中定義的變量使用時(shí)坚嗜,父接口才會(huì)初始化。另外诗充,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的 <clinit>() 方法苍蔬。(對(duì)比第二點(diǎn))
5. <clinit>()
多線程安全
虛擬機(jī)會(huì)保證一個(gè)類的 <clinit>()
方法在多線程環(huán)境下被正確的加鎖和同步,如果多個(gè)線程同時(shí)初始化一個(gè)類蝴蜓,只會(huì)有一個(gè)線程執(zhí)行這個(gè)類的 <clinit>()
方法碟绑,其它線程都會(huì)阻塞等待,直到活動(dòng)線程執(zhí)行 <clinit>()
方法完畢茎匠。如果在一個(gè)類的 <clinit>()
方法中有耗時(shí)的操作格仲,就可能造成多個(gè)進(jìn)程阻塞,在實(shí)際過程中此種阻塞很隱蔽诵冒。
init和clinit方法的不同
init和clinit方法執(zhí)行時(shí)機(jī)不同
init是對(duì)象構(gòu)造器方法凯肋,也就是說在程序執(zhí)行 new 一個(gè)對(duì)象調(diào)用該對(duì)象類的 constructor 方法時(shí)才會(huì)執(zhí)行init方法,而clinit是類構(gòu)造器方法汽馋,也就是在jvm進(jìn)行類加載—–驗(yàn)證—-解析—–初始化侮东,中的初始化階段jvm會(huì)調(diào)用clinit方法圈盔。
init和clinit方法執(zhí)行目的不同
init is the (or one of the) constructor(s) for the instance, and non-static field initialization.
clinit are the static initialization blocks for the class, and static field initialization.
- init是instance實(shí)例構(gòu)造器,對(duì)非靜態(tài)變量解析初始化
- 而clinit是class類構(gòu)造器對(duì)靜態(tài)變量悄雅,靜態(tài)代碼塊進(jìn)行初始化驱敲。
參考文章
周志明,深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐煤伟,機(jī)械工業(yè)出版社
【深入理解JVM】:類加載機(jī)制
Jvm筆記總結(jié)(八):虛擬機(jī)類加載機(jī)制