您好,登錄后才能下訂單哦!
JVM 類加載機制及雙親委派模型是什么,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
Java 程序是如何跑起來的呢,如何從一個 .java 源文件到控制臺的輸出結果?
要回答類似的問題就需要學習虛擬機類加載機制。
整體的流程
Java 中的所有類,必須被裝載到 jvm 中才能運行,這個裝載工作是由 jvm 中的類加載器完成的,類加載器所做的工作實質是把類文件從硬盤讀取到內存中,JVM 在加載類的時候,都是通過 ClassLoader 的 loadClass()方法來加載 class 的,loadClass 使用雙親委派模型。
以上流程中出現了很多陌生的名詞,本篇文章就是解析這些名詞,當你回頭再看這句話便豁然開朗。
先解析一下這張圖,圖表示類的整個聲明周期,類從被加載到虛擬機內存開始,到卸載出內存為止,包含 7 個階段,其中驗證、準備、解析 3 個階段統稱為連接。
加載、驗證、準備、初始化和卸載這 5 個階段的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之后開始,這是為了支持 Java 語言的運行時綁定(動態綁定或晚期綁定)。
裝載兩個字說起來簡單,但是對于 JVM 來說,這是個復雜的流程,也就是虛擬機的類加載機制:虛擬機把描述類的數據從 Class 文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型。
加載
這里所說的「加載」是「類加載」過程的一個階段,「類加載」描述的是整個過程,「加載」僅表示「類加載」的第一階段,需要完成以下三件事情:
通過一個類的全限定名來獲取定義此類的二進制字節流
將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
在內存中生成一個代表該類的 java.lang.Class 對象,作為方法區這個類的各種數據的訪問入口
說這么多其實就完成了一件事情:根據一個類的名字(全限定名)在內存中生成一個 Class 對象,注意 Class 對象不是關鍵字 new 出來的那個對象,Class 是一種類型,表示的是一個對象的運行時類型信息。
接下來的三個階段,都屬于連接(Linking)。加載階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始。
連接 - 驗證
驗證是為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。如果驗證到輸入的字節流不符合 Class 文件格式的約束,虛擬機就會拋出一個 java.lang.VerifyError 異常或其子類異常。
驗證階段大致完成 4 個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
連接 - 準備
準備階段是正式為類變量(static 修飾的變量)分配內存并設置類變量初始值的極端,這些變量所使用的內存都將在方法區中進行分配。注意此時進行內存分配的僅包括類變量,而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在 Java 堆中。
并且這里提到的初始值是指零值,每種基本數據類型都有對應的零值。
假設一個類變量的定義為: public static int value = 123
那這個變量在準備階段過后的初始值是 0 而不是 123,把 value 賦值為 123 的動作將在初始化階段才會執行
連接 - 解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程
符號引用:只包含語義信息,不涉及具體實現,以一組符號來描述引用目標,是字面量;符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經加載到內存中。
直接引用:與具體實現息息相關,是直接指向目標的指針;直接引用是可以直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。
初始化
初始化階段,才真正開始執行類中定義的 Java 程序代碼(或者說是字節碼)
在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源。
也就是我們通常理解的賦初始值以及執行靜態代碼塊。
類與類加載器
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。
比較兩個類是否「相等」,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等
加載器的種類(從開發人員的角度)
啟動類加載器(Bootstrap ClassLoader):負責將存放在
擴展類加載器(Extension ClassLoader):負責加載
應用程序類加載器(Application ClassLoader):也稱為系統類加載器,負責加載用戶類路徑(ClassPath)上所指定的類庫。如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器
上圖所示的類加載器之間的層次關系,稱為類加載器的雙親委派模型。
雙親委派模型除了要求頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。這里的類加載器之間的父子關系一般不會以繼承的關系類實現,而是都使用組合關系來復用父加載器的代碼。
雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
為什么要使用雙親委派模型
借用一個例子:黑客自定義一個 java.lang.String 類,該 String 類具有系統的String 類一樣的功能,只是在某個函數稍作修改。比如 equals 函數,這個函數經常使用,如果在這這個函數中,黑客加入一些“病毒代碼”。并且通過自定義類加載器加入到 JVM 中。此時,如果沒有雙親委派模型,那么JVM就可能誤以為黑客自定義的 java.lang.String 類是系統的 String 類,導致“病毒代碼”被執行。
而有了雙親委派模型,黑客自定義的 java.lang.String 類永遠都不會被加載進內存。因為首先是最頂端的類加載器加載系統的 java.lang.String 類,最終自定義的類加載器無法加載 java.lang.String 類。
也就是說,無論那一個類加載器去加載一個系統中已有的類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此系統里在程序的各種類加載器環境中都是同一個類。
雙親委派模型是如何實現的
實現雙親委派的代碼都幾種在 java.lang.ClassLoader 的 loadClass() 方法中:先檢查是否已經被加載過,若沒有加載則調用父加載器的 loadClass() 方法,若父加載器為空則默認使用啟動類加載器作為父加載器。如果父加載器加載失敗,拋出 ClassNotFoundException 異常后,再調用自己的 findClass() 方法進行加載。(看源碼后發現這里的拋出異常是被吞了,catch 之后不會做任何操作)
雙親委派模型并不是一個強制性的約束模型,而是 Java 設計者推薦給開發者的類加載器的實現方式。大部分的類加載器都遵循這個模型,但雙親委派模型也可以被破壞,破壞并不是不好,而是在有足夠意義和理由的情況下,突破已有的規則進行創建,實現特定的功能。
三種破壞雙親委派模型的方式
重寫 loadClass() 方法
逆向使用類加載器,引入線程上下文類加載器
追求程序的動態性:代碼熱替換、模塊熱部署等技術
看完上述內容,你們掌握JVM 類加載機制及雙親委派模型是什么的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。