摘要:驗證驗證是連接階段的第一步,這一階段的目的是為了確保文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。字節(jié)碼驗證通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的符合邏輯的。
看過這篇文章,大廠面試你「雙親委派模型」,硬氣的說一句,你怕啥?
讀該文章姿勢打開手頭的 IDE,按照文章內(nèi)容及思路進行代碼跟蹤與思考
手頭沒有 IDE,先收藏,回頭看 (萬一哪次面試問了呢)
需要查看和拷貝代碼,點擊文章末尾出「閱讀原文」
文章內(nèi)容相對較長,所以添加了目錄,如果你希望對 Java 的類加載過程有個更深入的了解,同時增加自己的面試技能點,請耐心讀完......
雙親委派模型在介紹這個Java技術(shù)點之前,先試著思考以下幾個問題:
為什么我們不能定義同名的 String 的 java 文件?
多線程的情況下,類的加載為什么不會出現(xiàn)重復(fù)加載的情況?
熱部署的原理是什么?
下面代碼,虛擬機是怎樣初始化注冊 Mysql 連接驅(qū)動(Driver)的?
想理解以上幾個問題的前提是了解類加載時機與過程, 這篇文章將會以非常詳細的解讀方式來回答以上幾個問題
類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統(tǒng)稱為連接(Linking)。如圖所示
加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定(也稱為動態(tài)綁定或晚期綁定)
加載在加載階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機需要完成以下3件事情:
通過一個類的全限定名來獲取定義此類的二進制字節(jié)流(并沒有指明要從一個Class文件中獲取,可以從其他渠道,譬如:網(wǎng)絡(luò)、動態(tài)生成、數(shù)據(jù)庫等);
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu);
在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口;
加載階段和連接階段(Linking)的部分內(nèi)容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進行的動作,仍然屬于連接階段的內(nèi)容,這兩個階段的開始時間仍然保持著固定的先后順序。
驗證驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
驗證階段大致會完成4個階段的檢驗動作:
文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范;例如:是否以魔術(shù)0xCAFEBABE開頭(當class文件以二進制形式打開,會看到這個文件頭,cafebabe)、主次版本號是否在當前虛擬機的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型。
元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規(guī)范的要求;例如:這個類是否有父類,除了java.lang.Object之外。
字節(jié)碼驗證:通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。
符號引用驗證:確保解析動作能正確執(zhí)行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗證,那么可以考慮采用-Xverifynone參數(shù)來關(guān)閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
準備準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配。這時候進行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在堆中。其次,這里所說的初始值通常情況下是數(shù)據(jù)類型的零值,假設(shè)一個類變量的定義為:
有通常情況就有特殊情況,這里的特殊是指:
解析解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符7類符號引用進行。
初始化在介紹初始化時,要先介紹兩個方法:
在編譯生成class文件時,會自動產(chǎn)生兩個方法,一個是類的初始化方法
clinit>:在jvm第一次加載class文件時調(diào)用,包括靜態(tài)變量初始化語句和靜態(tài)塊的執(zhí)行
類初始化階段是類加載過程的最后一步,到了初始化階段,才真正開始執(zhí)行類中定義的java程序代碼。在準備極端,變量已經(jīng)付過一次系統(tǒng)要求的初始值,而在初始化階段,則根據(jù)程序猿通過程序制定的主管計劃去初始化類變量和其他資源,或者說:初始化階段是執(zhí)行類構(gòu)造器
那么去掉報錯的那句,改成下面:
輸出結(jié)果:1
為什么輸出結(jié)果是 1,在準備階段我們知道 i=0,然后類初始化階段按照順序執(zhí)行,首先執(zhí)行 static 塊中的 i=0,接著執(zhí)行 static賦值操作i=1, 最后在 main 方法中獲取 i 的值為1
由于父類的
接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成
虛擬機會保證一個類的
讓我們來驗證上面的加載規(guī)則
驗證 1: 虛擬機會保證在子類() 方法執(zhí)行之前,父類的() 方法方法已經(jīng)執(zhí)行完畢
輸出結(jié)果
SSClass SuperClass init! 123
驗證 2: 通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化(我的理解是數(shù)組的父類是Object)
輸出結(jié)果:無驗證 3: 常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類的初始化
輸出結(jié)果:
hello world
驗證小結(jié)
虛擬機規(guī)范嚴格規(guī)定了有且只有5中情況(jdk1.7)必須對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):
遇到 new, getstatic, putstatic, invokestatic 這些字節(jié)碼指令時,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
使用 java.lang.reflect 包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。
當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化。
當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
當使用jdk1.7動態(tài)語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結(jié)果REF_getstatic, REF_putstatic, REF_invokeStatic 的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進行初始化,則需要先出觸發(fā)其初始化。
有了這個加載規(guī)則的印象,雙親委派模型就很好理解了,別著急,繼續(xù)向下看, 你會發(fā)現(xiàn)你的理解層面提高了
雙親委派模型剛看到這個詞匯的時候我是完全懵懂的狀態(tài),其實就是定義了 JVM 啟動的時候類的加載規(guī)則, 大家要按規(guī)矩辦事,好辦事,來看下圖:
所謂雙親委派是指每次收到類加載請求時,先將請求委派給父類加載器完成(所有加載請求最終會委派到頂層的Bootstrap ClassLoader加載器中),如果父類加載器無法完成這個加載(該加載器的搜索范圍中沒有找到對應(yīng)的類),子類嘗試自己加載, 如果都沒加載到,則會拋出 ClassNotFoundException 異常, 看到這里其實就解釋了文章開頭提出的第一個問題,父加載器已經(jīng)加載了JDK 中的 String.class 文件,所以我們不能定義同名的 String java 文件。
為什么會有這樣的規(guī)矩設(shè)定?
因為這樣可以避免重復(fù)加載,當父親已經(jīng)加載了該類的時候,就沒有必要 ClassLoader 再加載一次??紤]到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時使用自定義的String來動態(tài)替代java核心api中定義的類型,這樣會存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況,因為String 已經(jīng)在啟動時就被引導(dǎo)類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,除非你改變 JDK 中 ClassLoader 搜索類的默認算法。
我們發(fā)現(xiàn)除了啟動類加載器(BootStrap ClassLoader),每個類都有其"父類"加載器
?? 其實這里的父子關(guān)系是組合模式,不是繼承關(guān)系來實現(xiàn)
從圖中可以看到類 AppClassLoader 和 ExtClassLoader 都繼承 URLClassLoader, 而 URLClassLoader 又繼承 ClassLoader, 在 ClassLoader 中有一個屬性
在通過構(gòu)造函數(shù)實例化 AppClassLoader 和 ExtClassLoader 的時候都要傳入一個 classloader 作為當前 classloader 的 parent
頂層ClassLoader有幾個函數(shù)很關(guān)鍵,先有個印象
指定保護域(protectionDomain),把ByteBuffer的內(nèi)容轉(zhuǎn)換成 Java 類,這個方法被聲明為final的
把字節(jié)數(shù)組 b中的內(nèi)容轉(zhuǎn)換成 Java 類,其開始偏移為off,這個方法被聲明為final的
查找指定名稱的類
鏈接指定的類類加載器責(zé)任范圍
上面我們提到每個加載器都有對應(yīng)的加載搜索范圍
Bootstrap ClassLoader:這個加載器不是一個Java類,而是由底層的c++實現(xiàn),負責(zé)在虛擬機啟動時加載Jdk核心類庫(如:rt.jar、resources.jar、charsets.jar等)以及加載后兩個類加載器。這個ClassLoader完全是JVM自己控制的,需要加載哪個類,怎么加載都是由JVM自己控制,別人也訪問不到這個類
Extension ClassLoader:是一個普通的Java類,繼承自ClassLoader類,負責(zé)加載{JAVA_HOME}/jre/lib/ext/目錄下的所有jar包。
App ClassLoader:是Extension ClassLoader的子對象,負責(zé)加載應(yīng)用程序classpath目錄下的所有jar和class文件。
大家自行運行這個文件,就可以看到每個類加載器加載的文件了
兩種類的加載方式通常用這兩種方式來動態(tài)加載一個 java 類,Class.forName() 與 ClassLoader.loadClass() 但是兩個方法之間也是有一些細微的差別
Class.forName() 方式
查看Class類的具體實現(xiàn)可知,實質(zhì)上這個方法是調(diào)用原生的方法:
形式上類似于Class.forName(name,true,currentLoader)。 綜上所述,Class.forName 如果調(diào)用成功會:
保證一個Java類被有效得加載到內(nèi)存中;
類默認會被初始化,即執(zhí)行內(nèi)部的靜態(tài)塊代碼以及保證靜態(tài)屬性被初始化;
默認會使用當前的類加載器來加載對應(yīng)的類。
ClassLoader.loadClass方式
如果采用這種方式的類加載策略,由于雙親托管模型的存在,最終都會將類的加載任務(wù)交付給Bootstrap ClassLoader進行加載。跟蹤源代碼,最終會調(diào)用原生方法:
與此同時,與上一種方式的最本質(zhì)的不同是,類不會被初始化,只有顯式調(diào)用才會進行初始化。綜上所述,ClassLoader.loadClass 如果調(diào)用成功會:
類會被加載到內(nèi)存中;
類不會被初始化,只有在之后被第一次調(diào)用時類才會被初始化;
之所以采用這種方式的類加載,是提供一種靈活度,可以根據(jù)自身的需求繼承ClassLoader類實現(xiàn)一個自定義的類加載器實現(xiàn)類的加載。(很多開源Web項目中都有這種情況,比如tomcat,struct2,jboss。原因是根據(jù)Java Servlet規(guī)范的要求,既要Web應(yīng)用自己的類的優(yōu)先級要高于Web容器提供的類,但同時又要保證Java的核心類不被任意覆蓋,此時重寫一個類加載器就很必要了)
雙親委派模型源碼分析 Launcher分析類加載器源碼要從 sun.misc.Launcher.class 文件看起, 關(guān)鍵代碼已添加注釋,同時可以在此類中看到 ExtClassLoader 和 AppClassLoader 的定義,也驗證了我們上文提到的他們不是繼承關(guān)系,而是通過指定 parent 屬性來形成的組合模型
進入上面第25行的 loadClass 方法中
我們看到方法有同步塊(synchronized), 這也就解釋了文章開頭第2個問題,多線程情況不會出現(xiàn)重復(fù)加載的情況。同時會詢問parent classloader是否有加載,如果沒有,自己嘗試加載。
URLClassLoader中的 findClass方法:
借用網(wǎng)友的一個加載時序圖來解釋整個過程更加清晰:
Java本身有一套資源管理服務(wù)JNDI,是放置在rt.jar中,由啟動類加載器加載的。以對數(shù)據(jù)庫管理JDBC為例,java給數(shù)據(jù)庫操作提供了一個Driver接口:
然后提供了一個DriverManager來管理這些Driver的具體實現(xiàn):
這里省略了大部分代碼,可以看到我們使用數(shù)據(jù)庫驅(qū)動前必須先要在DriverManager中使用registerDriver()注冊,然后我們才能正常使用。
不破壞雙親委派模型的情況(不使用JNDI服務(wù))我們看下mysql的驅(qū)動是如何被加載的:
核心就是這句Class.forName()觸發(fā)了mysql驅(qū)動的加載,我們看下mysql對Driver接口的實現(xiàn):
可以看到,Class.forName()其實觸發(fā)了靜態(tài)代碼塊,然后向DriverManager中注冊了一個mysql的Driver實現(xiàn)。這個時候,我們通過DriverManager去獲取connection的時候只要遍歷當前所有Driver實現(xiàn),然后選擇一個建立連接就可以了。
破壞雙親委派模型的情況在JDBC4.0以后,開始支持使用spi的方式來注冊這個Driver,具體做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明當前使用的Driver是哪個,然后使用的時候就直接這樣就可以了:
可以看到這里直接獲取連接,省去了上面的Class.forName()注冊過程。
現(xiàn)在,我們分析下看使用了這種spi服務(wù)的模式原本的過程是怎樣的:
第一,從META-INF/services/java.sql.Driver文件中獲取具體的實現(xiàn)類名“com.mysql.jdbc.Driver”
第二,加載這個類,這里肯定只能用class.forName("com.mysql.jdbc.Driver")來加載
好了,問題來了,Class.forName()加載用的是調(diào)用者的Classloader,這個調(diào)用者DriverManager是在rt.jar中的,ClassLoader是啟動類加載器,而com.mysql.jdbc.Driver肯定不在
那么,這個問題如何解決呢?按照目前情況來分析,這個mysql的drvier只有應(yīng)用類加載器能加載,那么我們只要在啟動類加載器中有方法獲取應(yīng)用程序類加載器,然后通過它去加載就可以了。這就是所謂的線程上下文加載器。
文章前半段提到線程上下文類加載器可以通過 Thread.setContextClassLoaser() 方法設(shè)置,如果不特殊設(shè)置會從父類繼承,一般默認使用的是應(yīng)用程序類加載器
很明顯,線程上下文類加載器讓父級類加載器能通過調(diào)用子級類加載器來加載類,這打破了雙親委派模型的原則
現(xiàn)在我們看下DriverManager是如何使用線程上下文類加載器去加載第三方j(luò)ar包中的Driver類的,先來看源碼:
使用時,我們直接調(diào)用DriverManager.getConnection() 方法自然會觸發(fā)靜態(tài)代碼塊的執(zhí)行,開始加載驅(qū)動然后我們看下ServiceLoader.load()的具體實現(xiàn):
繼續(xù)向下看構(gòu)造函數(shù)實例化 ServiceLoader 做了哪些事情:
查看 reload() 函數(shù):
繼續(xù)查看LazyIterator構(gòu)造器,該類同樣實現(xiàn)了Iterator接口:
實例化到這里我們也將上下文得到的類加載器實例化到這里,來回看ServiceLoader 重寫的 iterator() 方法:
上面next() 方法調(diào)用了lookupIterator.next(),這個lookupIterator 就是剛剛實例化的 LazyIterator(); 來看next方法
繼續(xù)查看nextService 方法:
終于到這里了,在上面 nextService函數(shù)中第8行調(diào)用了c = Class.forName(cn, false, loader) 方法,我們成功的做到了通過線程上下文類加載器拿到了應(yīng)用程序類加載器(或者自定義的然后塞到線程上下文中的),同時我們也查找到了廠商在子級的jar包中注冊的驅(qū)動具體實現(xiàn)類名,這樣我們就可以成功的在rt.jar包中的DriverManager中成功的加載了放在第三方應(yīng)用程序包中的類了同時在第16行完成Driver的實例化,等同于new Driver(); 文章開頭的問題在理解到這里也迎刃而解了
JAVA熱部署實現(xiàn)首先談一下何為熱部署(hotswap),熱部署是在不重啟 Java 虛擬機的前提下,能自動偵測到 class 文件的變化,更新運行時 class 的行為。Java 類是通過 Java 虛擬機加載的,某個類的 class 文件在被 classloader 加載后,會生成對應(yīng)的 Class 對象,之后就可以創(chuàng)建該類的實例。默認的虛擬機行為只會在啟動時加載類,如果后期有一個類需要更新的話,單純替換編譯的 class 文件,Java 虛擬機是不會更新正在運行的 class。如果要實現(xiàn)熱部署,最根本的方式是修改虛擬機的源代碼,改變 classloader 的加載行為,使虛擬機能監(jiān)聽 class 文件的更新,重新加載 class 文件,這樣的行為破壞性很大,為后續(xù)的 JVM 升級埋下了一個大坑。
另一種友好的方法是創(chuàng)建自己的 classloader 來加載需要監(jiān)聽的 class,這樣就能控制類加載的時機,從而實現(xiàn)熱部署。
熱部署步驟:
銷毀自定義classloader(被該加載器加載的class也會自動卸載);
更新class
使用新的ClassLoader去加載class
JVM中的Class只有滿足以下三個條件,才能被GC回收,也就是該Class被卸載(unload):
該類所有的實例都已經(jīng)被GC,也就是JVM中不存在該Class的任何實例。
加載該類的ClassLoader已經(jīng)被GC。
該類的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法
自定義類加載器要創(chuàng)建用戶自己的類加載器,只需要繼承java.lang.ClassLoader類,然后覆蓋它的findClass(String name)方法即可,即指明如何獲取類的字節(jié)碼流。
如果要符合雙親委派規(guī)范,則重寫findClass方法(用戶自定義類加載邏輯);要破壞的話,重寫loadClass方法(雙親委派的具體邏輯實現(xiàn))。
感謝與參考非常感謝以下博文的作者,通過反復(fù)拜讀來了解雙親委派模型的原理
https://blog.csdn.net/u014634...
https://www.cnblogs.com/aspir...
https://www.cnblogs.com/gdpuz...
https://www.jianshu.com/p/09f...
https://www.cnblogs.com/yahok...
推薦閱讀面試還不知道 BeanFactory 和 ApplicationContext 的區(qū)別?
Spring Bean 生命周期之"我從哪里來?",懂得這個很重要
Spring Bean 生命周期之"我要到哪里去?"
如何設(shè)計好的RESTful API
輕松高效玩轉(zhuǎn)DTO(Data Transfer Object)
后續(xù)會出一系列文章點亮上圖,同時進行 Spring 知識點解釋與串聯(lián),在工作中充分利用 Spring 的特性
另外,還會推出 Java 多線程與 ElasticSearch 相關(guān)內(nèi)容
歡迎持續(xù)關(guān)注公眾號:「日拱一兵」前沿 Java 技術(shù)干貨分享
高效工具匯總
面試問題分析與解答
技術(shù)資料領(lǐng)取
持續(xù)關(guān)注,帶你像讀偵探小說一樣輕松趣味學(xué)習(xí) Java 技術(shù)棧相關(guān)知識
閱讀原文
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/75299.html
摘要:原文地址游客前言金三銀四,很多同學(xué)心里大概都準備著年后找工作或者跳槽。最近有很多同學(xué)都在交流群里求大廠面試題。 最近整理了一波面試題,包括安卓JAVA方面的,目前大廠還是以安卓源碼,算法,以及數(shù)據(jù)結(jié)構(gòu)為主,有一些中小型公司也會問到混合開發(fā)的知識,至于我為什么傾向于混合開發(fā),我的一句話就是走上編程之路,將來你要學(xué)不僅僅是這些,豐富自己方能與世接軌,做好全棧的裝備。 原文地址:游客kutd...
摘要:最終形成可以被虛擬機最直接使用的類型的過程就是虛擬機的類加載機制。即重寫一個類加載器的方法驗證驗證是連接階段的第一步,這一階段的目的是為了確保文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。 《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版》讀書筆記與常見相關(guān)面試題總結(jié) 本節(jié)常見面試題(推薦帶著問題閱讀,問題答案在文中都有提到): 簡單說說類加載過...
摘要:如問到是否使用某框架,實際是是問該框架的使用場景,有什么特點,和同類可框架對比一系列的問題。這兩個方向的區(qū)分點在于工作方向的側(cè)重點不同。 [TOC] 這是一份來自嗶哩嗶哩的Java面試Java面試 32個核心必考點完全解析(完) 課程預(yù)習(xí) 1.1 課程內(nèi)容分為三個模塊 基礎(chǔ)模塊: 技術(shù)崗位與面試 計算機基礎(chǔ) JVM原理 多線程 設(shè)計模式 數(shù)據(jù)結(jié)構(gòu)與算法 應(yīng)用模塊: 常用工具集 ...
閱讀 1754·2021-11-25 09:43
閱讀 2700·2019-08-30 15:53
閱讀 1852·2019-08-30 15:52
閱讀 2925·2019-08-29 13:56
閱讀 3350·2019-08-26 12:12
閱讀 596·2019-08-23 17:58
閱讀 2184·2019-08-23 16:59
閱讀 965·2019-08-23 16:21