類的加載指的是將類的.class文件
中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個java.lang.Class對象
,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,而且向Java程序員提供了訪問方法區內的數據結構的接口。java
類加載器並不須要等到某個類被「首次主動使用」時再加載它,JVM規範容許類加載器在預料某個類將要被使用時就預先加載它,若是在預先加載的過程當中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)若是這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。程序員
加載.class文件的方式:web
其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。數據庫
查找並加載類的二進制數據加載時類加載過程的第一個階段,在加載階段,虛擬機須要完成如下三件事情:數組
相對於類加載的其餘階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動做)是可控性最強的階段,由於開發人員既可使用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載。緩存
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在Java堆中也建立一個java.lang.Class類的對象,這樣即可以經過該對象訪問方法區中的這些數據。tomcat
驗證:確保被加載的類的正確性安全
驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成4個階段的檢驗動做:服務器
驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。網絡
爲類的靜態變量分配內存,並將其初始化爲默認值
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有如下幾點須要注意:
假設一個類變量的定義爲:public static int value = 3;
那麼變量value在準備階段事後的初始值爲0,而不是3,由於這時候還沒有開始執行任何Java方法,而把value賦值爲3的public static指令是在程序編譯後,存放於類構造器<clinit>()方法之中的,因此把value賦值爲3的動做將在初始化階段纔會執行。
這裏還須要注意以下幾點:
假設上面的類變量value被定義爲: public static final int value = 3;
編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲3。咱們能夠理解爲static final常量在編譯期就將其結果放入了調用它的類的常量池中
把類中的符號引用轉換爲直接引用。
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,能夠是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
初始化,爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:
JVM初始化步驟:
類初始化時機:只有當對類的主動使用的時候纔會致使類的初始化,類的主動使用包括如下六種:
在以下幾種狀況下,Java虛擬機將結束生命週期
JVM類加載機制的特色:
全盤負責
,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其餘Class也將由該類加載器負責載入,除非顯示使用另一個類加載器來載入父類委託
,先讓父類加載器試圖加載該類,只有在父類加載器沒法加載該類時才嘗試從本身的類路徑中加載該類緩存機制
,緩存機制將會保證全部加載過的Class都會被緩存,當程序中須要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是爲何修改了Class後,必須重啓JVM,程序的修改纔會生效雙親委派機制
一般類加載器能夠大體劃分爲如下三類,它們遵循雙親委派規則:
啓動類加載器:Bootstrap ClassLoader
,負責加載存放在JDKjrelib(JDK表明JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,而且能被虛擬機識別的類庫(如rt.jar,全部的java.開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器是沒法被Java程序直接引用的。擴展類加載器:Extension ClassLoader
,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載JDKjrelibext目錄中,或者由java.ext.dirs系統變量指定的路徑中的全部類庫(如javax.開頭的類),開發者能夠直接使用擴展類加載器。應用程序類加載器:System ClassLoader
,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。
雙親委派優勢
委託模式主要爲了確保Java核心庫的組件老是正確地被加載,避免重複加載。優先使用「引導類載入器」,而後是「擴展類載入器」,爲了在出現和JAVA核心庫同名資源的時候,加載的老是正確的系統組件。好比說就算我在本身的CLASSPATH下寫了一個惡意的java.lang.Object類,也不會被載入。JVM載入的永遠是系統核心庫中的正確的java.lang.Object類。
雙親委派可見性
子類加載器能夠看到父類加載器加載的類,而反之則不行。
正由於這個前提,當「啓動類加載器」加載了Java核心庫,「系統類加載器」後續加載的庫才能夠訪問Java核心庫。反過來,若是當「系統類加載器」加載了某些自開發的類,「啓動類加載器」中是沒法直接訪問的。
雙親委派模型不是一個強制性的約束模型,雙親委派模型也有不太適用的時候,這時根據具體的狀況咱們就要破壞這種機制,下面介紹兩種破壞雙親委派的狀況:
Thread.currentThread().getContextClassLoader();
從方法名字來看,應該是獲取當前上下文的類加載器。
Java 提供了不少服務提供者接口(Service Provider Interface,SPI),容許第三方爲這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
這些 SPI 的接口由 Java 核心庫來提供,而這些 SPI 的實現代碼則是做爲 Java 應用所依賴的 jar 包被包含進類路徑(CLASSPATH)裏。SPI接口中的代碼常常須要加載具體的實現類。那麼問題來了,SPI的接口是Java核心庫的一部分,是由啓動類加載器(Bootstrap Classloader)來加載的;SPI的實現類是由系統類加載器(System ClassLoader)來加載的。引導類加載器是沒法找到 SPI 的實現類的,由於依照雙親委派模型,BootstrapClassloader沒法委派SystemClassLoader來加載類。
ClassLoader A -> System class loader -> Extension class loader -> Bootstrap class loader
那麼委派鏈左邊的ClassLoader就能夠很天然的使用右邊的ClassLoader所加載的類。但若是狀況要反過來,是右邊的ClassLoader所加載的代碼須要反過來去找委派鏈靠左邊的ClassLoader去加載東西怎麼辦呢?沒轍,雙親委派是單向的,沒辦法反過來從右邊找左邊。
因而,Thread就把當前的類加載器給保存下來了,其餘加載器須要的時候,就經過當前線程的加載器獲取到。每個Thread都有一個相關聯的Context ClassLoader(由native方法創建的除外),能夠經過Thread.setContextClassLoader()方法設置。若是你沒有主動設置,Thread默認集成Parent Thread的 Context ClassLoader(注意,是parent Thread 不是父類)。若是你整個應用中都沒有對此做任何處理,那麼 全部的Thread都會以System ClassLoader做爲Context ClassLoader。知道這一點很重要,由於從web服務器,java企業服務器使用一些複雜並且精巧的ClassLoader結構去實現諸如JNDI、線程池和熱部署等功能以來,這種簡單的狀況愈加的少見了,通常都會使用特定的classloader來設置thread context classLoader。
破壞委派雙親模型就是因爲用戶追求動態性致使的,「動態性」就是指代碼熱替換、模塊熱部署等,就是但願程序不須要重啓就能夠更新class文件,最典型的例子就是SpringBoot的熱部署和OSGi。這裏拿OSGi舉例,OSGi實現模塊化熱部署的關鍵就是它自定義類加載機制的實現,每個程序模塊(OSGi中稱爲Bundle)都有本身的類加載器,當須要更換一個Bundle時,就把Bundle連同類加載器一塊兒換掉實現熱部署。因此,在OSGi環境下,類加載器再也不是層次模型,而是網狀模型。
當OSGi收到一個類加載的時候會按照如下的順序進行搜索:
java.*
開頭的類委派給父類加載器加載前文咱們瞭解了Java中類加載器的運行方式;但主流的Web服務器都會有本身的一套類加載器,爲何呢?由於對於服務器來講他要本身解決一些問題:
顯然,若是Tomcat使用默認的類加載機制是沒法知足上述要求的:
Tomcat的類加載流程如上圖:
目錄結構大體爲:
所以就解決了上面的四個問題: