類加載機制

1. 類加載機制

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,而且向Java程序員提供了訪問方法區內的數據結構的接口。java

類加載器並不須要等到某個類被「首次主動使用」時再加載它,JVM規範容許類加載器在預料某個類將要被使用時就預先加載它,若是在預先加載的過程當中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)若是這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。程序員

加載.class文件的方式:web

  • 從本地系統中直接加載
  • 經過網絡下載.class文件
  • 從zip,jar等歸檔文件中加載.class文件
  • 從專有數據庫中提取.class文件
  • 將Java源文件動態編譯爲.class文件

2. 類的生命週期

其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。數據庫

2.1. 加載

查找並加載類的二進制數據加載時類加載過程的第一個階段,在加載階段,虛擬機須要完成如下三件事情:數組

  • 經過一個類的全限定名來獲取其定義的二進制字節流。
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  • 在Java堆中生成一個表明這個類的java.lang.Class對象,做爲對方法區中這些數據的訪問入口。

相對於類加載的其餘階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動做)是可控性最強的階段,由於開發人員既可使用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載。緩存

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在Java堆中也建立一個java.lang.Class類的對象,這樣即可以經過該對象訪問方法區中的這些數據。tomcat

2.2. 驗證

驗證:確保被加載的類的正確性安全

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成4個階段的檢驗動做:服務器

  • 文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
  • 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。
  • 字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
  • 符號引用驗證:確保解析動做能正確執行。

驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。網絡

2.3. 準備

爲類的靜態變量分配內存,並將其初始化爲默認值

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有如下幾點須要注意:

  1. 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。
  2. 這裏所設置的初始值一般狀況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

假設一個類變量的定義爲:public static int value = 3;

那麼變量value在準備階段事後的初始值爲0,而不是3,由於這時候還沒有開始執行任何Java方法,而把value賦值爲3的public static指令是在程序編譯後,存放於類構造器<clinit>()方法之中的,因此把value賦值爲3的動做將在初始化階段纔會執行。

這裏還須要注意以下幾點:

  • 對基本數據類型來講,對於類變量(static)和全局變量,若是不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來講,在使用前必須顯式地爲其賦值,不然編譯時不經過。
  • 對於同時被static和final修飾的常量,必須在聲明的時候就爲其顯式地賦值,不然編譯時不經過;而只被final修飾的常量則既能夠在聲明時顯式地爲其賦值,也能夠在類初始化時顯式地爲其賦值,總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值。
  • 對於引用數據類型reference來講,如數組引用、對象引用等,若是沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null。
  • 若是在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。
  1. 若是類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變量value就會被初始化爲ConstValue屬性所指定的值。

假設上面的類變量value被定義爲: public static final int value = 3;

編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲3。咱們能夠理解爲static final常量在編譯期就將其結果放入了調用它的類的常量池中

2.4. 解析

把類中的符號引用轉換爲直接引用。

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,能夠是任何字面量。

直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

2.5. 初始化

初始化,爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:

  1. 聲明類變量是指定初始值
  2. 使用靜態代碼塊爲類變量指定初始值

JVM初始化步驟:

  1. 假如這個類尚未被加載和鏈接,則程序先加載並鏈接該類
  2. 假如該類的直接父類尚未被初始化,則先初始化其直接父類
  3. 假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機:只有當對類的主動使用的時候纔會致使類的初始化,類的主動使用包括如下六種:

  1. 建立類的實例,也就是new的方式
  2. 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  3. 調用類的靜態方法
  4. 反射(如Class.forName(「com.shengsiyuan.Test」))
  5. 初始化某個類的子類,則其父類也會被初始化
  6. Java虛擬機啓動時被標明爲啓動類的類(Java Test),直接使用java.exe命令來運行某個主類

2.6. 結束生命週期

在以下幾種狀況下,Java虛擬機將結束生命週期

  • 執行了System.exit()方法
  • 程序正常執行結束
  • 程序在執行過程當中遇到了異常或錯誤而異常終止
  • 因爲操做系統出現錯誤而致使Java虛擬機進程終止

3. Jvm類加載-雙親委派

JVM類加載機制的特色
  1. 全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其餘Class也將由該類加載器負責載入,除非顯示使用另一個類加載器來載入
  2. 父類委託,先讓父類加載器試圖加載該類,只有在父類加載器沒法加載該類時才嘗試從本身的類路徑中加載該類
  3. 緩存機制,緩存機制將會保證全部加載過的Class都會被緩存,當程序中須要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是爲何修改了Class後,必須重啓JVM,程序的修改纔會生效
雙親委派機制

一般類加載器能夠大體劃分爲如下三類,它們遵循雙親委派規則:

  1. 啓動類加載器:Bootstrap ClassLoader,負責加載存放在JDKjrelib(JDK表明JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,而且能被虛擬機識別的類庫(如rt.jar,全部的java.開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器是沒法被Java程序直接引用的。
  2. 擴展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載JDKjrelibext目錄中,或者由java.ext.dirs系統變量指定的路徑中的全部類庫(如javax.開頭的類),開發者能夠直接使用擴展類加載器。
  3. 應用程序類加載器:System ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。

  1. 當AppClassLoader加載一個class時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
  2. 當ExtClassLoader加載一個class時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
  3. 若是BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用ExtClassLoader來嘗試加載;
  4. 若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,若是AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。
雙親委派優勢

委託模式主要爲了確保Java核心庫的組件老是正確地被加載,避免重複加載。優先使用「引導類載入器」,而後是「擴展類載入器」,爲了在出現和JAVA核心庫同名資源的時候,加載的老是正確的系統組件。好比說就算我在本身的CLASSPATH下寫了一個惡意的java.lang.Object類,也不會被載入。JVM載入的永遠是系統核心庫中的正確的java.lang.Object類。

雙親委派可見性

子類加載器能夠看到父類加載器加載的類,而反之則不行。

正由於這個前提,當「啓動類加載器」加載了Java核心庫,「系統類加載器」後續加載的庫才能夠訪問Java核心庫。反過來,若是當「系統類加載器」加載了某些自開發的類,「啓動類加載器」中是沒法直接訪問的。

4. 破壞雙親委派

雙親委派模型不是一個強制性的約束模型,雙親委派模型也有不太適用的時候,這時根據具體的狀況咱們就要破壞這種機制,下面介紹兩種破壞雙親委派的狀況:

4.1. 線程上下文類加載器

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。

image

4.2. 自定義類加載器

破壞委派雙親模型就是因爲用戶追求動態性致使的,「動態性」就是指代碼熱替換、模塊熱部署等,就是但願程序不須要重啓就能夠更新class文件,最典型的例子就是SpringBoot的熱部署和OSGi。這裏拿OSGi舉例,OSGi實現模塊化熱部署的關鍵就是它自定義類加載機制的實現,每個程序模塊(OSGi中稱爲Bundle)都有本身的類加載器,當須要更換一個Bundle時,就把Bundle連同類加載器一塊兒換掉實現熱部署。因此,在OSGi環境下,類加載器再也不是層次模型,而是網狀模型。

當OSGi收到一個類加載的時候會按照如下的順序進行搜索:

  1. 將以 java.* 開頭的類委派給父類加載器加載
  2. 不然,將委派列表名單內的類委派給父類加載器加載
  3. 不然,將Import列表中的類委派給Export這個類的Bundle的類加載器加載
  4. 不然,查找當前Bundle的ClassPath,使用本身的類加載器加載
  5. 檢查Fragment Bundle中是否能夠加載
  6. 查找Dynamic Import列表的Bundle
  7. 若以上都沒有進行類加載,則加載失敗

5. Tomcat的類加載模式

前文咱們瞭解了Java中類加載器的運行方式;但主流的Web服務器都會有本身的一套類加載器,爲何呢?由於對於服務器來講他要本身解決一些問題:

  1. 部署在同一個Web容器上的兩個Web應用程序所使用的Java類庫能夠實現相互隔離。兩個不一樣的應用程序可能會依賴同一個第三方類庫的不一樣版本,不能要求一個類庫在一個服務器中只有一份,服務器應當保證兩個應用程序的類庫能夠互相獨立使用。
  2. 部署在同一個Web容器上的兩個Web應用程序所使用的相同的類庫相同的版本能夠互相共享。例如,用戶可能有10個使用Spring組織的應用程序部署在同一臺服務器上,若是把10份Spring分別存放在各個應用程序的隔離目錄中,將會是很大的資源浪費——這主要倒不是浪費磁盤空間的問題,而是指類庫在使用時都要被加載到Web容器的內存,若是類庫不能共享,虛擬機的方法區就會很容易出現過分膨脹的風險。
  3. Web容器須要儘量地保證自身的安全不受部署的Web應用程序影響。Web容器也有用Java實現的,那麼確定不能把Web容器的類庫和程序的類庫弄混。
  4. 支持jsp的web容器,要支持熱部署。咱們知道運行jsp時實際上會先將jsp翻譯成servlet,再編譯爲.class再在虛擬機運行起來再返回給客戶端。而咱們在編寫jsp時,當tomcat服務器正在運行的時候,咱們直接在jsp中修改代碼時並不須要重啓服務器,這就是達到了動態加載類的效果。

顯然,若是Tomcat使用默認的類加載機制是沒法知足上述要求的:

  1. 沒法加載兩個相同類庫的不一樣版本的,由於默認類加載只在意權限定類名,第一條不行。
  2. 能夠實現。
  3. 默認類加載只在意權限定類明,因此第三條不行。
  4. 前文咱們說過,JVM肯定是否爲同一個類對象會要求類和類加載器都相同,默認的確定不行,但咱們能夠想到當改變jsp代碼的時候就改一次類加載器。

image

Tomcat的類加載流程如上圖:

  • CommonClassLoader能加載的類均可以被Catalina ClassLoader和SharedClassLoader使用
  • 而CatalinaClassLoader和Shared ClassLoader本身能加載的類則與對方相互隔離。
  • WebAppClassLoader可使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
  • 而JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並經過再創建一個新的Jsp類加載器來實現JSP文件的HotSwap功能。

目錄結構大體爲:

  • /common目錄中:類庫可被Tomcat和全部的Web應用程序共同使用。
  • /server目錄中:類庫可被Tomcat使用,對全部的Web應用程序都不可見。
  • /shared目錄中:類庫可被全部的Web應用程序共同使用,但對Tomcat本身不可見。
  • /WebApp/WEB-INF目錄中:類庫僅僅能夠被此Web應用程序使用,對Tomcat和其餘Web應用程序都不可見。

所以就解決了上面的四個問題:

  1. 部署在同一個Web容器上的兩個Web應用程序所使用的Java類庫能夠實現相互隔離:各個WebAppClassLoader實例之間相互隔離
  2. 部署在同一個Web容器上的兩個Web應用程序所使用的相同的類庫相同的版本能夠互相共享:能夠放在Common或Shared目錄下讓這些程序共享
  3. Web容器須要儘量地保證自身的安全不受部署的Web應用程序影響:CatalinaClassLoader加載web服務器須要的類庫,WebAppClassLoader只能獲得SharedClassLoader的類庫
  4. 支持jsp的web容器,要支持熱部署:每當改變jsp時,更新JasperClassLoader
相關文章
相關標籤/搜索