1.前言
前面的幾個(gè)章節(jié)了解了JVM的基礎(chǔ)知識(shí),直到了JVM的底層結(jié)構(gòu)及內(nèi)存的回收策略,這章接著學(xué)習(xí)JVM加載類的過程
2.目錄
3.類的加載過程
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),轉(zhuǎn)化解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型
- 整個(gè)生命周期包括:加載(Loading),驗(yàn)證(Verification),準(zhǔn)備(Preparation),解析(Preparation),初始化(Initialization),使用(Using)和卸載(Unloading)7個(gè)階段.其中準(zhǔn)備,驗(yàn)證,解析3個(gè)部分統(tǒng)稱為連接(Linking)
- 加載,驗(yàn)證,準(zhǔn)備,初始化和卸載的執(zhí)行順序是確定的解析階段則在某些情況下可以在初始化階段之后再開始,這就是Java語言的運(yùn)行時(shí)綁定(也稱動(dòng)態(tài)綁定或晚期綁定)
3.1.加載階段
加載階段有以下三個(gè)步驟:
- 通過類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流
- 將這個(gè)字節(jié)流所代表的的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口
其中獲取二進(jìn)制字節(jié)流可以通過Class文件,ZIP包,網(wǎng)絡(luò),運(yùn)行時(shí)(動(dòng)態(tài)代理),JSP生成,數(shù)據(jù)庫等途徑獲取
需要注意的是數(shù)組類的加載,數(shù)組類并不通過類加載器加載,而是由Java虛擬機(jī)直接創(chuàng)建,但是數(shù)組類還是要依靠類加載器進(jìn)行加載
這些二進(jìn)制字節(jié)流加載完成之后,按照指定的格式存放于方法區(qū)內(nèi)(Java7及以前方法區(qū)位于永久代,Java8位于元空間).然后再方法區(qū)生成一個(gè)比較特殊的java.lang.Class對(duì)象,用來作為程序訪問方法區(qū)中這些類型數(shù)據(jù)的外部接口
3.2.驗(yàn)證階段
驗(yàn)證的目的是確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全
- 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范;比如,是否以魔術(shù)0XCAFEBABE開頭,主次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi),常量池中的常量是否有不被支持的類型.只有驗(yàn)證通過才會(huì)進(jìn)入方法區(qū)進(jìn)行存儲(chǔ)
- 元數(shù)據(jù)驗(yàn)證:對(duì)自己餓嗎描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求;比如,是否有父類(除Object類),父類是否為final修飾,是否實(shí)現(xiàn)抽象方法或接口,重載是否正確等
- 字節(jié)碼驗(yàn)證:通過字節(jié)流和控制流分析,確定程序語義是合法的,符合邏輯的.比如,保證數(shù)據(jù)類型與指令正常配合工作,指令不會(huì)跳轉(zhuǎn)到方法體外的字節(jié)碼上,方法體中的類型轉(zhuǎn)換是有效的等
- 符號(hào)引用驗(yàn)證:在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候進(jìn)行驗(yàn)證,可以看做是對(duì)類自身以外的信息(常量池中的各種符號(hào)引用)進(jìn)行匹配性的校驗(yàn).常見的異常比如:
java.lang.NoSuchMethodError,java.lang.NoSuchFiledError
等
3.3.準(zhǔn)備階段
主被截?cái)嘀饕钦菫轭愖兞糠峙鋬?nèi)存并設(shè)置類變量的初始值,變量所使用的內(nèi)存豆?jié){在方法區(qū)中進(jìn)行分配
此處的類變量指的是被static修飾的變量,不包含實(shí)例變量,實(shí)例變量在對(duì)象實(shí)例化階段分配在堆中
public static String a = "A";
并且,變量的初始化值并不是類中定義的值,而是該變量所屬類型的默認(rèn)值
當(dāng)然,也有特殊情況,比如當(dāng)變量被final修飾時(shí):此時(shí),該字段屬性是ConstantValue
時(shí),會(huì)在主被截?cái)喑跏蓟癁橹付ǖ闹?/p>
3.4.解析階段
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程.解析動(dòng)作主要針對(duì)類或接口,字段,類方法,接口方法,方法類型,方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行
這里我們看一下字段解析,也就是最開始第一道面試題.當(dāng)獲取SubClass的屬性a時(shí),首先會(huì)查找SubClass本身是否包含該字段,如果包含則直接返回引用,查找結(jié)束
否則,如果SubClass類實(shí)現(xiàn)了接口或繼承了父類,那么則遞歸搜索各個(gè)接口和父類,找到匹配的屬性則返回,查找結(jié)束
否則,查找失敗,拋出java.lang.NoSuchFieldError
異常.如果返回成功了,但是是權(quán)限校驗(yàn)失敗,也就是無該字段的訪問權(quán)限,則拋出java.lang.illegalAccessError
異常
其它形式的解析,就不在這里一一說明了
3.5.初始化階段
初始化階段才是真正執(zhí)行類中定義的Java程序代碼(字節(jié)碼).在此階段會(huì)根據(jù)代碼進(jìn)行類變量和其他資源的初始化,或者可以從另一個(gè)角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()
方法的過程
<clinit>()
方法是由編譯器自動(dòng)收集類中的所有變量的復(fù)制動(dòng)作和靜態(tài)語句塊(static語句塊)中的語句合并生成的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊中可以賦值,但是不能訪問
注釋掉錯(cuò)誤提示行,打印結(jié)果為A
,在準(zhǔn)備階段屬性a的值為null
,然后類初始化按照順序執(zhí)行,首先執(zhí)行static塊中的a="B"
,接著執(zhí)行a="A"
的復(fù)制操作,此時(shí)值為A
,當(dāng)main
方法調(diào)用打印時(shí)則為A
<clinit>()
方法與實(shí)例構(gòu)造器<init>()
方法不同,它不需要顯示地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類<clinit>()
方法執(zhí)行之前,父類的<clinit>()
方法已經(jīng)執(zhí)行完畢
由于父類的<clinit>()
方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作
<clinit>()
方法對(duì)于類或者接口來說并不是行必須的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對(duì)變量賦值操作,那么編譯器可以不為這個(gè)類生產(chǎn)<clinit>()
方法
接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的復(fù)制操作,因此接口與類一樣都會(huì)生成<clinit>()
方法.但接口與類不同的是,執(zhí)行接口的<clinit>()
方法不需要先執(zhí)行父接口的<clinit>()
方法.只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化.另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()
方法
虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()
方法在多線程環(huán)境中被正確的加鎖,同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()
方法,其它線程都需要等待,直到活動(dòng)線程執(zhí)行<clinit>()
方法執(zhí)行完畢.如果在一個(gè)類的<clinit>()
方法中有耗時(shí)很長(zhǎng)的操作,就可能造成逗哥線程阻塞,在實(shí)際應(yīng)用中這種阻塞旺旺是隱藏的
- <clinit>:在jvm第一次加載class文件時(shí)調(diào)用,包括靜態(tài)變量初始化語句和靜態(tài)塊的執(zhí)行
- <init>:在實(shí)例創(chuàng)建出來的時(shí)候調(diào)用,包括調(diào)用new操作符,調(diào)用Class或
java.lang.reflect.Constructor
對(duì)象的newInstance()方法,調(diào)用任何有對(duì)象的clone()
方法,通過java.io.ObjectInputStream
類的getObject()
方法反序列化
3.6.虛擬機(jī)規(guī)范初始化
虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且只有5中情況(jdk1.7)必須對(duì)類進(jìn)行"初始化"(而加載、驗(yàn)證、準(zhǔn)備自然需要在此之前開始):
- 遇到new,getStatic,putStatic,invokeStatic這失調(diào)字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化.生成這4條指令的最常見的Java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候,讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾,已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候
- 使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化啥容。
- 當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類
- 當(dāng)使用jdk1.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ā)其初始化
4.小結(jié)
這章主要講解了類的加載過程,對(duì)底層數(shù)據(jù)的存儲(chǔ),分布和加載有了大致的了解,知識(shí)點(diǎn)比較多,有一個(gè)整體的認(rèn)識(shí)就可以,下一個(gè)講解類加載器及雙親委派機(jī)制
原文:https://www.choupangxia.com/2019/10/27/interview-jvm-load-01/