JVM學(xué)習(xí)第二篇思考:一個(gè)Java代碼是怎么運(yùn)行起來(lái)的-下篇
JVM學(xué)習(xí)第二篇思考:一個(gè)Java代碼是怎么運(yùn)行起來(lái)的-下篇
在上一篇《JVM學(xué)習(xí)第一篇思考:一個(gè)Java代碼是怎么運(yùn)行起來(lái)的-上篇》中咱們知道類一個(gè)Java類的生命周期需要經(jīng)歷以下七個(gè)階段:類加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載。同時(shí),我們對(duì)每個(gè)階段都做了簡(jiǎn)單介紹。于是我們就得到了如下的:

編輯
今日目標(biāo):
jvm在什么時(shí)候會(huì)去加載一個(gè)類?
類加載器和雙親委派機(jī)制是什么?
上一篇問(wèn)題思路解析
jvm在什么時(shí)候會(huì)去加載一個(gè)類?
我們既然知道了一個(gè)Java類的生命周期。那么一個(gè)類在什么時(shí)候被加載呢?類加載的時(shí)機(jī)是什么?什么是主動(dòng)引用?什么是被動(dòng)引用呢?
在Java的虛擬機(jī)規(guī)范中,沒(méi)有對(duì)加載階段作出明確約束。但是在初始化階段,Java虛擬機(jī)嚴(yán)格規(guī)定了有且只有在以下幾種情況下,類必須立即進(jìn)行初始化的(注意初始化階段在類生命周期中哪個(gè)階段)
1.1:使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候
當(dāng)我們使用new關(guān)鍵字來(lái)創(chuàng)建(實(shí)例化)對(duì)象的時(shí)候,讀取和設(shè)置類的靜態(tài)的變量(static int = 1;)、靜態(tài)非字面值常量(靜態(tài)字面值常量除外。如:static Person )的時(shí)候,調(diào)用靜態(tài)方法的時(shí)候(如:staitc String getName();)。
1.2:在進(jìn)行反射調(diào)用的時(shí)候

編輯
1.3:當(dāng)初始化一個(gè)類的是,如果父類沒(méi)有進(jìn)行初始化,需要先初始化父類
這個(gè)很好理解,當(dāng)你想要使用一個(gè)類的子類的時(shí)候,父類沒(méi)有被初始化加載,子類是不能直接使用的
1.4:?jiǎn)?dòng)程序所使用的的main方法所在的類
這個(gè)也很好理解,因?yàn)閙ain方法是程序的入口。程序要啟動(dòng),不加載對(duì)應(yīng)的類,怎么啟動(dòng)?
1.5:當(dāng)使用了1.7新特性-動(dòng)態(tài)語(yǔ)音支持時(shí)候
簡(jiǎn)單總結(jié):
1:使用關(guān)鍵字new一個(gè)對(duì)象的是
2:對(duì)類中的靜態(tài)字段(非final修飾的)進(jìn)行set或get的時(shí)候
(當(dāng)被final修飾后,回顧下final關(guān)鍵字的特性)
3:調(diào)用了一個(gè)類中的靜態(tài)方法的時(shí)候
都會(huì)觸發(fā)加載需要的類加載。上面這些加載是主動(dòng)加載的,所以被稱為主動(dòng)引用。
當(dāng)然,有了主動(dòng)引用當(dāng)然會(huì)有被動(dòng)引用,被動(dòng)引用有以下幾種種情況:
通過(guò)子類引用了父類的靜態(tài)字段。只會(huì)觸發(fā)父類的初始化,而不會(huì)觸發(fā)子類的初始化。
根據(jù)Java面向?qū)ο蟮亩鄳B(tài)特性,子類可以執(zhí)行父類的。這里通過(guò)子類引用父類的靜態(tài)字段,操作的是父類,而非子類自己。所以子類不會(huì)被觸發(fā)初始化。
定義對(duì)象為數(shù)組或集合的時(shí)候,這種情況,不會(huì)觸發(fā)該類的初始化
數(shù)組或集合雖然也是對(duì)象,但是這些對(duì)象是由JVM處理的。所以,不會(huì)觸發(fā)的
類A引用了類B的static final常量不會(huì)導(dǎo)致B初始化
這里需要注意,靜態(tài)常量必須是 字面值常量。不然還是會(huì)觸發(fā)B的初始化
這個(gè)怎么理解呢?比如:static final str = "132";這個(gè)時(shí)候,A類雖然引用了B類的str.但是因?yàn)閟tr這個(gè)字面值被fianl關(guān)鍵字修飾了,此時(shí)的“132”進(jìn)入了常量池中了。
通過(guò)類名獲取該類的Class對(duì)象,不會(huì)觸發(fā)類的初始化
比如:sysout(Person.class);這個(gè)方法的時(shí)候,是不會(huì)調(diào)用類的初始化

編輯
通過(guò)Class.forName加載指定類的時(shí)候,如果指定了參數(shù)initialize=false時(shí)候
這個(gè)好理解,因?yàn)樵O(shè)置了懶加載,所以不會(huì)里面被初始化的
通過(guò)ClassLoader默認(rèn)的loadClass方法,也不會(huì)觸發(fā)初始化動(dòng)作
類加載器和雙親委派機(jī)制是什么?
一個(gè)類既然有加載,那么它是怎么被加載的?這個(gè)時(shí)候,類加載器就該出場(chǎng)了。
類加載器
類加載器分類:
引導(dǎo)類加載器
bootstrap class loader:引導(dǎo)類加載器
1:該加載器加載的是我們Java的核心庫(kù).路徑為:JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path這兩個(gè)路徑下的內(nèi)容。
2:加載擴(kuò)展類、應(yīng)用程序的類加載器(如果有用戶自定義的類加載器的話,也加載)。并為這類類加載器指定他們的父類加載器。
ps:rt.jar是Java的runtim需要的jar
擴(kuò)展類加載器
extensions class loader:擴(kuò)展類加載器
1:該類加載器加載的是Java擴(kuò)展庫(kù)。其加載的路徑為:
JAVA_HOME/jre/lib/ext/*.jar 或者java.ext.dirs路徑下的內(nèi)容。Java虛擬機(jī)會(huì)提供一個(gè)擴(kuò)展庫(kù)目錄。該類加載器就是在加載擴(kuò)展庫(kù)目錄中插找并加載對(duì)應(yīng)的Java的class文件
2:有sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)
應(yīng)用程序類加載器
application class loader:應(yīng)用程序類加載器
就是我們自己寫(xiě)的應(yīng)用
1:該加載器根據(jù)Java引用的類路徑(classpath,java.class.path路徑下的內(nèi)容)來(lái)加載類。
一般來(lái)說(shuō),Java應(yīng)用的類都是有這個(gè)類加載器來(lái)完成的
2:是由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)的
用戶自定義類加載器
開(kāi)發(fā)人員(我們)可以通過(guò)基礎(chǔ)java.lang.ClassLoader類的方式實(shí)現(xiàn)自己的類加載器,以滿足一些特殊需求。
classLoader類的作用:
java.class .ClassLoader類的基本職責(zé)就是根據(jù)一個(gè)指定的類的名稱,找到或者生成其對(duì)應(yīng)的字節(jié)碼代碼(class文件),然后從這些字節(jié)碼中定義出一個(gè)Java類,既是:java.lang.Class類的實(shí)例。
ClassLoader還負(fù)責(zé)加載Java應(yīng)用所需要的資源。比如圖片文件、配置文件等。
四種類加載器的關(guān)系:

編輯
上圖親子層級(jí)結(jié)構(gòu),就有一個(gè):雙親委派的機(jī)制。
擴(kuò)展:
比較兩個(gè)類是否相等的前提條件:兩個(gè)類必須是同一個(gè)類加載器加載的。如果沒(méi)有這個(gè)前提條件。比較的話,就是耍流氓。
比如:我要給你貨幣(法幣):30000.當(dāng)聽(tīng)到這個(gè)時(shí)候你是不是會(huì)很高興呢?但是如果這3W是越南盾呢?或者是天地銀行呢?所以,要有個(gè)前提條件。
雙親委派機(jī)制
雙親委派機(jī)制流程:
假設(shè)我們自己寫(xiě)的應(yīng)用程序(這個(gè)應(yīng)用的類加載器是:應(yīng)用程序類加載器)需要加載一個(gè)類來(lái)使用,那么應(yīng)用程序會(huì)先把這個(gè)查找類的任務(wù)交給他的父類加載器,也就是擴(kuò)展類加載器,擴(kuò)展類加載器收到查找任務(wù)后,轉(zhuǎn)手將這個(gè)查找類的任務(wù)交給了其父類加載器,也就是引導(dǎo)類加載器。如果引導(dǎo)類加載器在自己管轄范圍內(nèi)沒(méi)有查找到,所需要查找的類,就會(huì)向下推,把加載查找權(quán)力交個(gè)自己的子類加載器。依次類推。當(dāng)所有的類加載器都查找一遍,沒(méi)有找到的話,就會(huì)拋出classnotFund這個(gè)。
上面這么多文字,太復(fù)雜了,能具體點(diǎn)嗎?可以,咱們舉個(gè)例子:
比如上文中當(dāng)我們啟動(dòng)main方法的時(shí)候,使用了Son這個(gè)類。這個(gè)類的全路徑:com.kaigejava.jvm.Son.那么這個(gè)類是怎么被加載器加載的呢?

編輯
委派:
main方法所在的類的類加載器是:應(yīng)用程序類加載器 委派 其父類加載器==>擴(kuò)展類加載器
擴(kuò)展類加載器 委派 其父類加載器==>引導(dǎo)類加載器。
我們知道Son這個(gè)類編譯成class文件后,所在目錄是在classpath下的。所以,引導(dǎo)類啟動(dòng)加載器是肯定找不到的。
加載權(quán)限下放:
引導(dǎo)類加載器找不到后,就把 加載權(quán)限下放 給其子類加載器==>擴(kuò)展類加載器。同樣,擴(kuò)展類加載器在自己的歸屬范圍內(nèi)沒(méi)找到需要加載的類后。繼續(xù)加載權(quán)限下放
擴(kuò)展類加載器的子類加載器(應(yīng)用程序類加載器)就在自己歸屬地classpath下找到了son類。然后將其加載到虛擬機(jī)內(nèi)存中。
這種模式就是雙親委派機(jī)制。
一句話:往上捅。當(dāng)上面找不到后,在自己找找看。
好處:
這種機(jī)制是為了保證Java核心類庫(kù)(rt.jar)的安全性。比如:如果我們自己寫(xiě)了個(gè)String類。全類路徑:com.kaigejava.jvm.String這個(gè)類。因?yàn)橛辛穗p親委派機(jī)制的存在,我在Son類或者是在Father類中使用的String類都是同一個(gè)。
上一篇思考題解答
在上一篇結(jié)束的時(shí)候,凱哥(凱哥Java:kaigejava)留下了一個(gè)思考:
當(dāng)一個(gè)類存在父類的時(shí)候,初始化過(guò)程如下圖:

編輯
運(yùn)行結(jié)果:

編輯
從運(yùn)行的結(jié)果中我們可以確實(shí)可以得到如下結(jié)論:
當(dāng)一個(gè)類存在父類的時(shí)候,在初始化該類的時(shí)候,會(huì)先初始化其父類中的靜態(tài)代碼塊中方法,然后在執(zhí)行子類中靜態(tài)代碼塊,接著執(zhí)行父類非靜態(tài)代碼塊,接著父類構(gòu)造方法,接著子類非靜態(tài)代碼塊,最后才是子類的構(gòu)造方法。
具體的完整的初始化順序(存在父類情況下):
|
這個(gè)也是很常見(jiàn)的一個(gè)面試題。希望大家能夠記住。
當(dāng)是接口的時(shí)候,初始化過(guò)程:

編輯
運(yùn)行結(jié)果:

編輯
從運(yùn)行結(jié)果我們可以得到如下結(jié)論:
接口中不能使用靜態(tài)代碼塊,但可使用靜態(tài)變量。與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。只有父接口中定義的變量被使用時(shí)父接口才會(huì)被初始化,另外,接口的實(shí)現(xiàn)類在初始化時(shí)頁(yè)一樣不會(huì)執(zhí)行接口的<clinit>()方法。
本文思考題:
既然雙親委派機(jī)制有好處,那么在實(shí)際工作中,有沒(méi)有打破雙親委派機(jī)制的呢?
下一篇文章預(yù)告:
思路:我們已經(jīng)已經(jīng)知道了一個(gè)類怎么被加載到Java的虛擬機(jī)中了,同時(shí)我們 也知道了類加載器,雙親委派機(jī)制等。那么,我們寫(xiě)的類中聲明的變量(局部變量、全局變量)、聲明的對(duì)象等這些,在Java虛擬機(jī)中是怎么存在的?都存放在什么位置呢?這就是我們接下來(lái)要講解的:Java虛擬機(jī)的內(nèi)存模型。歡迎大家繼續(xù)和凱哥(kaigejava)一起學(xué)習(xí)。
凱哥有什么理解不對(duì)的地方,歡迎大家留言批評(píng)。凱哥在此先謝過(guò)各位
