定義:虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的java類型。類加載和鏈接的過程都是在運行期間完成的。 java
1. 類加載的生命週期:加載(Loading)-->驗證(Verification)-->準備(Preparation)-->解析(Resolution)-->初始化(Initialization)-->使用(Using)-->卸載(Unloading) 程序員
2. 加載:這有虛擬機自行決定。 web
3. 初始化階段: bootstrap
a) 遇到new、getstatic、putstatic、invokestatic這4個字節碼指令時,若是類沒有進行過初始化,出發初始化操做。 api
b) 使用java.lang.reflect包的方法對類進行反射調用時。 tomcat
c) 當初始化一個類的時候,若是發現其父類尚未執行初始化則進行初始化。 安全
d) 虛擬機啓動時用戶須要指定一個須要執行的主類,虛擬機首先初始化這個主類。 服務器
注意:接口與類的初始化規則在第三點不一樣,接口不要氣全部的父接口都進行初始化。 網絡
a) 加載階段的工做 數據結構
i. 經過一個類的全限定名來獲取定義此類的二進制字節流。
ii. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
iii. 在java堆中生成一個表明這個類的java.lang.Class對象,作爲方法區這些數據的訪問入口。
b) 加載階段完成以後二進制字節流就按照虛擬機所需的格式存儲在方區去中。
這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求。
a) 文件格式驗證:驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。
b) 元數據驗證:對字節碼描述的信息進行語義分析,以確保其描述的信息符合java語言規範的要求。
c) 字節碼驗證:這個階段的主要工做是進行數據流和控制流的分析。任務是確保被驗證類的方法在運行時不會作出危害虛擬機安全的行爲。
d) 符號引用驗證:這一階段發生在虛擬機將符號引用轉換爲直接引用的時候(解析階段),主要是對類自身之外的信息進行匹配性的校驗。目的是確保解析動做可以正常執行。
準備階段是正式爲變量分配內存並設置初始值,這些內存都將在方法區中進行分配,這裏的變量僅包括類標量不包括實例變量。
解析是虛擬機將常量池的符號引用替換爲直接引用的過程。
a) 符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任意形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。
b) 直接引用:直接引用能夠是直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。直接飲用是與內存佈局相關的。
c) 類或接口的解析
d) 字段的解析
e) 類方法解析
f) 接口方法解析
是根據程序員制定的主觀計劃區初始化變量和其餘資源,或者能夠從另一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。
當一個 JVM 啓動的時候,Java 缺省開始使用以下三種類型類裝入器:
啓動(Bootstrap)類加載器:引導類裝入器是用本地代碼實現的類裝入器,它負責將 <Java_Runtime_Home>/lib 下面的類庫加載到內存中。因爲引導類加載器涉及到虛擬機本地實現細節,開發者沒法直接獲取到啓動類加載器的引用,因此不容許直接經過引用進行操做。
標準擴展(Extension)類加載器:擴展類加載器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 實現的。它負責將
< Java_Runtime_Home >/lib/ext 或者由系統變量 java.ext.dir 指定位置中的類庫加載到內存中。開發者能夠直接使用標準擴展類加載器。
系統(System)類加載器:系統類加載器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫加載到內存中。開發者能夠直接使用系統類加載器。
除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器,這個將在後面單獨介紹。
a. Bootstrap ClassLoader/啓動類加載器
主要負責jdk_home/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar包裝入工做.
b. Extension ClassLoader/擴展類加載器
主要負責jdk_home/lib/ext目錄下的jar包或 -Djava.ext.dirs 指定目錄下的jar包裝入工做
c. System ClassLoader/系統類加載器
主要負責java -classpath/-Djava.class.path所指的目錄下的類與jar包裝入工做.
d. User Custom ClassLoader/用戶自定義類加載器(java.lang.ClassLoader的子類)
在程序運行期間, 經過java.lang.ClassLoader的子類動態加載class文件, 體現java動態實時類裝入特性.
在這裏,須要着重說明的是,JVM在加載類時默認採用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,若是父類加載器能夠完成類加載任務,就成功返回;只有父類加載器沒法完成此加載任務時,才本身去加載。關於虛擬機默認的雙親委派機制,咱們能夠從系統類加載器和標準擴展類加載器爲例做簡單分析。
圖一 標準擴展類加載器繼承層次圖
圖二 系統類加載器繼承層次圖
經過圖一和圖二咱們能夠看出,類加載器均是繼承自java.lang.ClassLoader抽象類。咱們下面咱們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:
經過進一步分析標準擴展類加載器(sun.misc.Launcher$ExtClassLoader)和系統類加載器(sun.misc.Launcher$AppClassLoader)的代碼以及其公共父類(java.net.URLClassLoader和java.security.SecureClassLoader)的代碼能夠看出,都沒有覆寫java.lang.ClassLoader中默認的加載委派規則---loadClass(…)方法。既然這樣,咱們就能夠經過分析java.lang.ClassLoader中的loadClass(String name)方法的代碼就能夠分析出虛擬機默認採用的雙親委派機制究竟是什麼模樣:
經過上面的代碼分析,咱們能夠對JVM採用的雙親委派類加載機制有了更感性的認識,下面咱們就接着分析一下啓動類加載器、標準擴展類加載器和系統類加載器三者之間的關係。可能你們已經從各類資料上面看到了以下相似的一幅圖片:
圖三 類加載器默認委派關係圖
上面圖片給人的直觀印象是系統類加載器的父類加載器是標準擴展類加載器,標準擴展類加載器的父類加載器是啓動類加載器,下面咱們就用代碼具體測試一下:
示例代碼:
說明:經過java.lang.ClassLoader.getSystemClassLoader()能夠直接獲取到系統類加載器。
經過以上的代碼輸出,咱們能夠斷定系統類加載器的父加載器是標準擴展類加載器,可是咱們試圖獲取標準擴展類加載器的父類加載器時確獲得了null,就是說標準擴展類加載器自己強制設定父類加載器爲null。咱們仍是藉助於代碼分析一下:
咱們首先看一下java.lang.ClassLoader抽象類中默認實現的兩個構造函數:
咱們再看一下ClassLoader抽象類中parent成員的聲明:
聲明爲私有變量的同時並無對外提供可供派生類訪問的public或者protected設置器接口(對應的setter方法),結合前面的測試代碼的輸出,咱們能夠推斷出:
1.系統類加載器(AppClassLoader)調用ClassLoader(ClassLoader parent)構造函數將父類加載器設置爲標準擴展類加載器(ExtClassLoader)。(由於若是不強制設置,默認會經過調用getSystemClassLoader()方法獲取並設置成系統類加載器,這顯然和測試輸出結果不符。)
2.擴展類加載器(ExtClassLoader)調用ClassLoader(ClassLoader parent)構造函數將父類加載器設置爲null。(由於若是不強制設置,默認會經過調用getSystemClassLoader()方法獲取並設置成系統類加載器,這顯然和測試輸出結果不符。)
如今咱們可能會有這樣的疑問:擴展類加載器(ExtClassLoader)的父類加載器被強制設置爲null了,那麼擴展類加載器爲何還能將加載任務委派給啓動類加載器呢?
圖四 標準擴展類加載器和系統類加載器成員大綱視圖
圖五擴展類加載器和系統類加載器公共父類成員大綱視圖
經過圖四和圖五能夠看出,標準擴展類加載器和系統類加載器及其父類(java.net.URLClassLoader和java.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中默認的加載委派規則---loadClass(…)方法。有關java.lang.ClassLoader中默認的加載委派規則前面已經分析過,若是父加載器爲null,則會調用本地方法進行啓動類加載嘗試。因此,圖三中,啓動類加載器、標準擴展類加載器和系統類加載器之間的委派關係事實上是仍就成立的。(在後面的用戶自定義類加載器部分,還會作更深刻的分析)。
以上已經簡要介紹了虛擬機默認使用的啓動類加載器、標準擴展類加載器和系統類加載器,並以三者爲例結合JDK代碼對JVM默認使用的雙親委派類加載機制作了分析。下面咱們就來看一個綜合的例子。首先在eclipse中創建一個簡單的java應用工程,而後寫一個簡單的JavaBean以下:
在現有當前工程中另外創建一測試類(ClassLoaderTest.java)內容以下:
測試一:
對應的輸出以下:
(說明:當前類路徑默認的含有的一個條目就是工程的輸出目錄)
測試二:
將當前工程輸出目錄下的…/classloader/test/bean/TestBean.class打包進test.jar剪貼到< Java_Runtime_Home >/lib/ext目錄下(如今工程輸出目錄下和JRE擴展目錄下都有待加載類型的class文件)。再運行測試一測試代碼,結果以下:
對比測試一和測試二,咱們明顯能夠驗證前面說的雙親委派機制,系統類加載器在接到加載classloader.test.bean.TestBean類型的請求時,首先將請求委派給父類加載器(標準擴展類加載器),標準擴展類加載器搶先完成了加載請求。
測試三:
將test.jar拷貝一份到< Java_Runtime_Home >/lib下,運行測試代碼,輸出以下:
測試三和測試二輸出結果一致。那就是說,放置到< Java_Runtime_Home >/lib目錄下的TestBean對應的class字節碼並無被加載,這其實和前面講的雙親委派機制並不矛盾。虛擬機出於安全等因素考慮,不會加載< Java_Runtime_Home >/lib存在的陌生類,開發者經過將要加載的非JDK自身的類放置到此目錄下期待啓動類加載器加載是不可能的。作個進一步驗證,刪除< Java_Runtime_Home >/lib/ext目錄下和工程輸出目錄下的TestBean對應的class文件,而後再運行測試代碼,則將會有ClassNotFoundException異常拋出。有關這個問題,你們能夠在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設置相應斷點運行測試三進行調試,會發現findBootstrapClass0()會拋出異常,而後在下面的findClass方法中被加載,當前運行的類加載器正是擴展類加載器(sun.misc.Launcher$ExtClassLoader),這一點能夠經過JDT中變量視圖查看驗證。
Java的鏈接模型容許用戶運行時擴展引用程序,既能夠經過當前虛擬機中預約義的加載器加載編譯時已知的類或者接口,又容許用戶自行定義類裝載器,在運行時動態擴展用戶的程序。經過用戶自定義的類裝載器,你的程序能夠裝載在編譯時並不知道或者還沒有存在的類或者接口,並動態鏈接它們並進行有選擇的解析。
運行時動態擴展java應用程序有以下兩個途徑:
這個方法其實在前面已經討論過,在後面的問題2解答中說明了該方法調用會觸發那個類加載器開始加載任務。這裏須要說明的是多參數版本的forName(…)方法:
這裏的initialize參數是很重要的,能夠以爲被加載同時是否完成初始化的工做(說明: 單參數版本的forName方法默認是不完成初始化的).有些場景下,須要將initialize設置爲true來強制加載同時完成初始化,例如典型的就是利用DriverManager進行JDBC驅動程序類註冊的問題,由於每個JDBC驅動程序類的靜態初始化方法都用DriverManager註冊驅動程序,這樣才能被應用程序使用,這就要求驅動程序類必須被初始化,而不僅僅被加載.
經過前面的分析,咱們能夠看出,除了和本地實現密切相關的啓動類加載器以外,包括標準擴展類加載器和系統類加載器在內的全部其餘類加載器咱們均可以當作自定義類加載器來對待,惟一區別是是否被虛擬機默認使用。前面的內容中已經對java.lang.ClassLoader抽象類中的幾個重要的方法作了介紹,這裏就簡要敘述一下通常用戶自定義類加載器的工做流程吧(能夠結合後面問題解答一塊兒看):
一、首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,若是已經裝載,直接返回;不然轉入步驟2
二、委派類加載請求給父類加載器(更準確的說應該是雙親類加載器,真個虛擬機中各類類加載器最終會呈現樹狀結構),若是父類加載器可以完成,則返回父類加載器加載的Class實例;不然轉入步驟3
三、調用本類加載器的findClass(…)方法,試圖獲取對應的字節碼,若是獲取的到,則調用defineClass(…)導入類型到方法區;若是獲取不到對應的字節碼或者其餘緣由失敗,返回異常給loadClass(…), loadClass(…)轉拋異常,終止加載過程(注意:這裏的異常種類不止一種)。
(說明:這裏說的自定義類加載器是指JDK 1.2之後版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯狀況下)
在Java中,一個類用其徹底匹配類名(fully qualified class name)做爲標識,這裏指的徹底匹配類名包括包名和類名。但在JVM中一個類用其全名和一個加載類ClassLoader的實例做爲惟一標識,不一樣類加載器加載的類將被置於不一樣的命名空間.咱們能夠用兩個自定義類加載器去加載某自定義類型(注意,不要將自定義類型的字節碼放置到系統路徑或者擴展路徑中,不然會被系統類加載器或擴展類加載器搶先加載),而後用獲取到的兩個Class實例進行java.lang.Object.equals(…)判斷,將會獲得不相等的結果。這個你們能夠寫兩個自定義的類加載器去加載相同的自定義類型,而後作個判斷;同時,能夠測試加載java.*類型,而後再對比測試一下測試結果。
Class.forName(String name)默認會使用調用類的類加載器來進行類加載。咱們直接來分析一下對應的jdk的代碼:
前面講過,在不指定父類加載器的狀況下,默認採用系統類加載器。可能有人以爲不明白,如今咱們來看一下JDK對應的代碼實現。衆所周知,咱們編寫自定義的類加載器直接或者間接繼承自java.lang.ClassLoader抽象類,對應的無參默認構造函數實現以下:
咱們再來看一下對應的getSystemClassLoader()方法的實現:
咱們能夠寫簡單的測試代碼來測試一下:
本機對應輸出以下:
因此,咱們如今能夠相信當自定義類加載器沒有指定父類加載器的狀況下,默認的父類加載器即爲系統類加載器。同時,咱們能夠得出以下結論:
即時用戶自定義類加載器不指定父類加載器,那麼,一樣能夠加載以下三個地方的類:
1. <Java_Runtime_Home>/lib下的類
2. < Java_Runtime_Home >/lib/ext下或者由系統變量java.ext.dir指定位置中的類
3. 當前工程類路徑下或者由系統變量java.class.path指定位置中的類
JVM規範中規定若是用戶自定義的類加載器將父類加載器強制設置爲null,那麼會自動將啓動類加載器設置爲當前用戶自定義類加載器的父類加載器(這個問題前面已經分析過了)。同時,咱們能夠得出以下結論:
即時用戶自定義類加載器不指定父類加載器,那麼,一樣能夠加載到<Java_Runtime_Home>/lib下的類,但此時就不可以加載<Java_Runtime_Home>/lib/ext目錄下的類了。
說明:問題3和問題4的推斷結論是基於用戶自定義的類加載器自己延續了java.lang.ClassLoader.loadClass(…)默認委派邏輯,若是用戶對這一默認委派邏輯進行了改變,以上推斷結論就不必定成立了,詳見問題5。
通常在JDK 1.2以前的版本才這樣作,並且事實證實,這樣作極有可能引發系統默認的類加載器不能正常工做。在JVM規範和JDK文檔中(1.2或者之後版本中),都沒有建議用戶覆寫loadClass(…)方法,相比而言,明確提示開發者在開發自定義的類加載器時覆寫findClass(…)邏輯。舉一個例子來驗證該問題:
經過前面的分析咱們已經知道,用戶自定義類加載器(WrongClassLoader)的默
認的類加載器是系統類加載器,可是如今問題4種的結論就不成立了。你們能夠簡
單測試一下,如今<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工
程類路徑上的類都加載不上了。
(說明:D:"classes"beans"Account.class物理存在的)
輸出結果:
這說明,連要加載的類型的超類型java.lang.Object都加載不到了。這裏列舉的因爲覆寫loadClass(…)引發的邏輯錯誤明顯是比較簡單的,實際引發的邏輯錯誤可能複雜的多。
將自定義類加載器代碼WrongClassLoader.Java作以上修改後,再運行測試代碼,輸出結果以下:
這說明,beans.Account加載成功,且是由自定義類加載器WrongClassLoader加載。
這其中的緣由分析,我想這裏就沒必要解釋了,你們應該能夠分析的出來了。
經過上面問題4和問題5的分析咱們應該已經理解,我的以爲這是自定義用戶類加載器時最重要的一點,但經常被忽略或者輕易帶過。有了前面JDK代碼的分析做爲基礎,我想如今你們均可以隨便舉出例子了。
事先儘可能準確理解待定義的類加載器要完成的加載任務,確保最大程度上可以獲取到對應的字節碼內容。
一是能夠直接調用ClassLoader.getSystemClassLoader()或者其餘方式獲取到系統類加載器(系統類加載器和擴展類加載器自己都派生自URLClassLoader),調用URLClassLoader中的getURLs()方法能夠獲取到;
二是能夠直接經過獲取系統屬性java.class.path 來查看當前類路徑上的條目信息 , System.getProperty("java.class.path")
方法之一:
本機對應輸出以下:
1, 每一個ClassLoader都維護了一份本身的名稱空間, 同一個名稱空間裏不能出現兩個同名的類。
2, 爲了實現java安全沙箱模型頂層的類加載器安全機制, java默認採用了 」 雙親委派的加載鏈 」 結構.
以下圖:
Class Diagram:
類圖中, BootstrapClassLoader是一個單獨的java類, 其實在這裏, 不該該叫他是一個java類。
由於, 它已經徹底不用java實現了。
它是在jvm啓動時, 就被構造起來的, 負責java平臺核心庫。(具體上面已經有介紹)
啓動類加載實現 (其實咱們不用關心這塊, 可是有興趣的, 能夠研究一下 ):
bootstrap classLoader 類加載原理探索
ClassLoader 類加載邏輯分析, 如下邏輯是除 BootstrapClassLoader 外的類加載器加載流程:
即在通常狀況下, 保證同一個類中所關聯的其餘類都是由當前類的類加載器所加載的.
上圖中 ClassLoader.getCallerClassLoader 就是獲得調用當前forName方法的類的類加載器
以上代碼摘自sun.misc.Launch的無參構造函數Launch()。
使用線程上下文類加載器, 能夠在執行線程中, 拋棄雙親委派加載鏈模式, 使用線程上下文裏的類加載器加載類.
典型的例子有, 經過線程上下文來加載第三方庫jndi實現, 而不依賴於雙親委派.
大部分java app服務器(jboss, tomcat..)也是採用contextClassLoader來處理web服務。
還有一些採用 hotswap 特性的框架, 也使用了線程上下文類加載器, 好比 seasar (full stack framework in japenese).
線程上下文從根本解決了通常應用不能違背雙親委派模式的問題.
使java類加載體系顯得更靈活.
隨着多核時代的來臨, 相信多線程開發將會愈來愈多地進入程序員的實際編碼過程當中. 所以,
在編寫基礎設施時, 經過使用線程上下文來加載類, 應該是一個很好的選擇.
固然, 好東西都有利弊. 使用線程上下文加載類, 也要注意, 保證多根鬚要通訊的線程間的類加載器應該是同一個,
防止由於不一樣的類加載器, 致使類型轉換異常(ClassCastException).
使用該接口, 能夠動態的加載class文件.
例如,
在jdk中, URLClassLoader是配合findClass方法來使用defineClass, 能夠從網絡或硬盤上加載class.
而使用類加載接口, 並加上本身的實現邏輯, 還能夠定製出更多的高級特性.
好比,
一個簡單的hot swap 類加載器實現:
這個類的做用是能夠從新載入同名的類, 可是, 爲了實現hotswap, 老的對象狀態
須要經過其餘方式拷貝到重載過的類生成的全新實例中來。(A類中的b實例)
而新實例所依賴的B類若是與老對象不是同一個類加載器加載的, 將會拋出類型轉換異常(ClassCastException).
爲了解決這種問題, HotSwapClassLoader自定義了load方法. 即當前類是由自身classLoader加載的, 而內部依賴的類
仍是老對象的classLoader加載的.
輸出