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