深度分析:Java虛擬機類加載機制、過程與類加載器

虛擬機類加載機制是把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型。

​ 須要注意的是 Java 語言與其餘編譯時須要進行鏈接工做的語言不通,它的鏈接過程是在程序運行期間完成的,這樣會在類加載時稍微增長一些性能開銷,可是卻能爲 Java 應用程序提供高度的靈活性。例如,若是編寫一個使用接口的應用程序,能夠等到運行時再指定其實際的實現。java

經過文章,你能夠了解到如下內容

類加載時機
類加載過程
類加載器與不足

1. 類加載的時機

類從被加載到虛擬機內存中開始,到卸載出內存,生命週期七個階段:程序員

加載、驗證、準備、解析、初始化、使用、卸載數據庫

其中加載、驗證、準備、初始化和卸載這五個階段時肯定的,由於 Java 支持運行時綁定,因此解析再某些狀況下能夠在初始化階段以後
安全

虛擬機規範規定有且只有四中狀況必須當即對類進行「初始化」(而加載、驗證、準備天然須要在此以前)網絡

遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節碼指令時,常見場景:
使用 new 關鍵字實例化對象
讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)
調用一個類的靜態方法
使用 java.lang.reflect 包的方法對類進行反射調用的時候
初始化一個類的時候,若是發現其父類尚未進行初始化,則須要先觸發其父類的初始化
當虛擬機啓動時,用戶須要指定一個要執行的主類(包含 mian() 方法的那個類),虛擬機會先初始化這個主類數據結構

2. 類加載過程

詳細介紹一下類加載的全過程,加載、驗證、準備、解析和初始化多線程

1. 加載

加載過程須要完成三件事情ide

經過一個類的全限定名來獲取定義此類的二進制字節流
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
在 Java 堆中生成一個表明這個類的 java.lang.Class 對象,做爲方法區這些數據的訪問入口
主要注意的是,第一點的二進制字節流能夠從文件、網絡、數據庫等地方獲取;加載階段也能夠由用戶自定義的類加載器完成;加載和鏈接的部份內容(文件格式校驗)是交叉進行的模塊化

2. 驗證

驗證的目的是爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身安全函數

(1)文件格式驗證:

是否以魔數 0xCAFEBABE 開頭
主、次版本號是否在當前虛擬機處理範圍以內
常量池的常量中是否有不被支持的常量類型(檢查常量 tag 標誌)
指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量
CONSTANT_Utf8_info 型的常量是否有不符合 UTF8 編碼的數據
Class文件中各個部分及文件自己是否有被刪除的或附加的其餘信息
(2) 元數據驗證:

這個類是否有父類(除了 java.lang.Object 以外,全部的類都應當有父類)
這個類的父類是否繼承了不容許被繼承的類(被 final 修飾的類)
若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法
類中的字段、方法是否與父類產生了矛盾(例如覆蓋了父類的 final 字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不一樣等)
(3) 字節碼驗證:

保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似這樣的狀況:在操做棧中放置了一個 int 類型的數據,使用時卻按 long 類型來加載入本地變量表中
保證跳轉指令不會跳轉到方法體之外的字節碼指令上
保證方法體中的類型轉換是有效的,例如能夠把一個子類對象賦值給與它毫無繼承關係、徹底不相干的一個數據類型,則是危險和不合法的
(4) 符號引用驗證:

符號引用中經過字符串描述的全限定名是否能找到對應的類
在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段
符號引用中的類、字段和方法的訪問性是否可被當前類訪問

3. 準備

準備階段是正式爲類變量分配內存並設置類變量初始值;內存分配僅包括類變量(被 static 修飾的變量),而不包括實例變量,而這裏的初始化,表明零值,即 static 變量初始化爲 0

4. 解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程

符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中
直接引用:直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句子柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同
解析的動做主要針對類或接口的解析、字段的解析、類方法解析、接口方法解析

5. 初始化

初始化階段是根據程序員經過程序制定的主觀計劃去初始化類變量的其餘資源;也能夠說是執行類構造器 <clinit>() 的過程。

<clinit>() 方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊 (static{}) 中的語句併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊中能夠賦值,可是不能訪問。
<clinit>() 方法與類的構造函數(或者說實例構造器 <init>() 方法)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的 <clinit>() 犯法執行以前,父類的 <clinit>() 方法已經執行完畢。所以在虛擬機中第一個被執行的 <clinit>() 犯法的類確定是 java.lang.Object。
因爲父類的 <clinit>() 犯法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做
<clinit>() 方法對於類或接口來講並非必須的,若是一個類鐘沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生產 <clinit> 方法
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成 <clinit> 方法。但接口與類不一樣,執行接口的 <clinit>() 方法不須要先執行父接口的 <clinit>() 方法。只有父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口 <clinit>() 方法
虛擬機會保證一個類的 <clinit>() 方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的 <clinit>() 方法,其餘線程都須要阻塞等待,直到活動線程執行 <clinit>() 方法完畢。

3. 類加載器與不足

類加載器只用於實現類的加載動做,可是對於任意一個類,都須要由加載它的類加載器和這個類自己一同肯定其在 Java 虛擬機中的惟一性。

雙親委派模型

虛擬機有兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用 C++ 語言實現,是虛擬機自身的一部分;另一種就是全部其餘的類加載器,這些類加載器都是由 Java 語言實現,獨立於虛擬機外部,而且所有繼承自抽象類 java.lang.ClassLoader

啓動類加載器(Bootstrap ClassLoader):將存放在 <JAVA_HOME>lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,而且是虛擬機識別的類庫加載到虛擬機內存中,啓動類加載器沒法被 Java 程序直接引用
擴展類加載器(Extension ClassLoader):這個加載器由 sun.misc.Launcher$ExtClassLoader 實現,它負責加載 <JAVA_HOME>libext 目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器
應用程序類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher$AppClassLoader 來實現,負責加載用戶類路徑上所指定的類庫
咱們的應用程序都是由這三種類加載器互相配合進行加載的,若是有必要,還能夠加入本身定義的類加載器

雙親委派模型的工做過程是:若是一個類加載器收到類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所須要的類)時,子加載器纔會唱誰本身去加載

好處:Java 類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。例如類 java.lang.Object,它放在 rt.jar 之中,不管哪個類加載器加載這個類,最終都是啓動類加載器去完成加載,所以 Object 類在程序的各類類加載器環境中都是同一個類;相反,若是沒有雙親委派模型,由各個類加載器自行去加載的話,若是用戶本身寫了一個名爲 java.lang.Object 的類,並放在 ClassPath 中,那系統將會出現多個不一樣 Object 類,形成程序混亂

破壞雙親委派模型

雙親委派模型是 Java 設計者們推薦給開發者的類加器實現方式,但也有例外,出現過三次大規模的破壞狀況

JDK1.2
​ 雙親委派模型是 JDK1.2以後才引入的,但類加載器和抽象類 java.lang.ClassLoader 則在 JDK 1.0 存在了,面對已經存在的用戶自定義類加載器的實現代碼,Java 設計者們引入雙親委派模型時不得不作出一些妥協。爲了向前兼容,JDK 1.2 以後的 java.lang.ClassLoader 添加了一個新的 protected 方法 findClass(),在此以前,用戶去繼承 java.lang.ClassLoader 的惟一目的是爲了重寫 loadClass() 方法,由於虛擬機在進行類加載的時候會調用加載器的私有方法 loadClassInternal(),而這個方法的惟一邏輯就是去調用本身的 loadClass()。

​ JDK1.2以後已不提倡用戶再去覆蓋 loadClass() 方法,由於 loadClass() 方法是雙親委派的實現,而另外提供了一個 protected 的 findClass() 方法,在 loadClass() 方法邏輯裏若是父類加載失敗,則會調用本身的 findClass() 方法來完成加載

線程上下文類加載器
​ 雙親委派模型解決了各個類加載器的基礎類的統一問題,可是若是基礎類又要調用回用戶的代碼的話,是作不到的,由於基礎類的類加載器只加載它目錄下的文件,可是基礎類調用到用戶的代碼的話,基礎類的類加載器就沒法加載用戶目錄下的類了

​ 爲了解決這個困境,Java 設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread context ClassLoader)。這個類加載器能夠經過 java.lang.Thread 類的 setContextClassLoader() 方法進行設置,若是建立線程時還未設置,它將會從父線程中繼承一個;若是在應用程序的全局範圍內都沒有設置過,那麼這個類加載器就是應用類加載器。

​ 有了線程上下文類加載器,原本基礎類(例如rt.jar)加載用戶的類時,只能經過自身的啓動類加載器完成的;如今即可以主動獲取線程上下文類加載器去完成用戶的類加載。對於SPI(Service Provider Interface)服務提供 API,即可以採用線程上下文類加載器,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等

OGSi
​ OGSi 是當前業界「事實上」的 Java 模塊化標準,而 OSGi 實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每個程序模塊(OSGi 中稱爲 Bundle)都有一個本身的類加載器,當須要更換一個 Bundle 時,就把 Bundle 連同類加載器一塊兒換掉以實現代碼的熱替換

​ 在 OGSi 環境下,類加載器再也不是雙親委派模型中的樹狀結構,而是進一步發展爲網狀結構,當類加載請求時,OSGi 將按照下面的順序進行搜索:

(1)將以 java.* 開頭的類,委派給父類加載器加載

(2)不然,將委派列表名單內的類,委派給父類加載器加載

(3)不然,將 Import 列表中的類,委派給 Export 這個類的 Bundle 的類加載器加載

(4)不然,查找當前 Bundle 的 ClassPath,使用本身的類加載器加載

(5)不然,查找類是否在本身的 Fragment Bundle 中,若是在,則委派給 Fragment Bundle 的類加載器加載

(6)不然,查找 Dynamic Import 列表的 Bundle,委派給對應 Bundle 的類加載器加載

(7)不然,類查找失敗

相關文章
相關標籤/搜索