一、前奏,舉個生活中的小栗子java
二、爲什麼Java類型加載、鏈接在程序運行期完成?數據庫
三、一個類在什麼狀況下才會被加載到JVM中?數組
什麼是主動使用、被動使用?代碼示例助你透徹理解類初始化的時機。緩存
四、類的加載(Loading)內幕透徹剖析tomcat
類加載作的那些事兒、雙親委派模型工做過程、ClassLoader源碼解析安全
五、Tomcat如何打破雙親委派模型的網絡
六、上下文類加載器深刻淺出剖析數據結構
七、最後總結併發
春節立刻要到了,你們是否是都在火燒眉毛的等着回家團圓了呢?框架
大春運早已啓動,回家的過程實際上是個「辛苦活」,有的同窗尚未買到票呢,矇眼狂奔終於搶到了,發現居然是個站票~,退了,連站票的機會都沒了吧?
昨天還聽一位同窗說:『嘿嘿,去年我提早就買到票了,可是... 可是... 去錯火車站了。。。尼瑪,當時那是啥心情啊~ 幸運的是後來又刷到票了,否則就真回不去了!』
回家大部分朋友都要乘坐交通工具,無論你乘坐什麼樣的交通工具出行,對於「交通管理」內部來講,最最重要的任務就是保障你們得出行安全。
那麼如何保障你們的出行安全呢?
乘坐地鐵、飛機等這些公共交通工具,必不可少的最重要的環節就是『安檢』,不是什麼東西均可以隨便讓你帶的,都是有明文規定的,好比易燃易爆、酒類等都是有限制的。
交通出行的大致過程,有點相似類文件加載到Java虛擬機(簡稱 JVM)的過程,程序中運行的各類類文件(好比Java、Kotlin),也是要必須通過『安檢』的,才能容許進入到JVM中的,一切都是爲了安全。
固然,安檢的標準是不一樣的。
接下來,咱們進入正題,一塊兒來看看類文件是如何被加載到JVM當中的。
上圖的對比只是爲了方便理解 ,抽象出來一層『安全檢查』,其實就是『類加載』的過程。
這個過程JVM當中約束了規範和標準,都會通過加載、驗證、準備、解析、初始化五個階段。
這裏必定要說一個概念,我的認爲對於理解類加載過程挺重要的。
更準確的說法,應該是類型
的加載過程,在Java代碼中,類型的加載、鏈接、初始化都是在程序運行時完成的。
這裏的類型,是指你在開發代碼時常見的class、interface、enum這些關鍵字的定義,並非指具體的class對象。
舉個🌰:
Object obj = new Object();
new出來的obj是Object類型嗎?固然不是,obj只是經過new建立出來的Object對象,而類型實際是Object類自己
。而要想建立Object對象的前提,必需要有類型的信息,才能在Java堆中建立出來。因此,這裏要明確區分開。
絕大多數狀況下,類型是提早編寫好的,好比Object類是由JDK已經提供的。另一些狀況是能夠在運行期間動態的生成出來,好比動態代理(程序運行期完成的)。
其實,運行區間能作這件事,就爲一些有創意的開發人員提供了不少的可能性。一切的文件都已經存在,程序運行的過程當中能夠採起一些特殊的處理方式把這些以前已經存在或者運行期生成出來的這些類型有機的裝配在一塊兒。
Java自己是一門靜態的語言,而他的不少特性又具備動態語言才能擁有的特質,也所以類型的加載、鏈接和初始化在運行期間完成起到了很大的幫助做用。
類型的加載:查找並加載類的二進制數據(字節碼文件),最多見的,是將類的Class文件從磁盤加載到內存中。
類型的鏈接:將類與類的關係肯定好,對於字節碼相關的處理、驗證、校驗在加載鏈接階段去完成的。字節碼自己能夠被人爲操縱的,也所以可能有惡意的可能性,因此須要校驗。
驗證:確保被加載類的正確性,就是要按照JVM規範定義的。
準備:爲類的靜態變量分配內存,並將其初始化爲默認值
class Test { public static int num = 1; }
上述代碼示例中的中間過程,在將類型加載到內存過程當中,num分配內存,首先設置爲0,1是在後續的初始化階段賦值給num變量。
符號引用: 間接的引用方式,經過一個符號的表示一個類引用了另外的類。 直接引用:直接引用到目標對象中的內存的位置
初始化階段:爲類的靜態變量賦予正確的初始值。
類型的初始化:好比一些靜態的變量的賦值是在初始化階段完成的。
### 三、一個類在什麼狀況下才會被加載到JVM中?
Java程序對類的使用方式可分爲兩種:
主動使用
被動使用
特別的重要:
全部的Java虛擬機實現必須在每一個類或接口被java程序首次主動使用時才初始化他們。
主動使用(八種狀況
):
1)建立類的實例,好比new一個對象
2)訪問某一個類或接口的靜態變量,或者對該靜態變量賦值 (訪問類的靜態變量的助記符getstatic,賦值是putstatic)。
3)調用類的靜態方法 (應用invokestatic助記符)。
4)使用java.lang.reflect包的方法對類型進行反射調用,好比:Class.forName(「com.test.Test") 經過反射的方式獲取類的Class對象。
5)初始化一個類的子類,好比有class Parent{}、子類class Child extends Parent{},當初始化Child類時也表示對Parent類的主動使用,Parent類也要所有初始化。
6)Java虛擬機啓動時被標註爲啓動類的類,即有main方法的類。
7)JDK1.7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic, REF_putStatic, REF_invokeStatic句柄對應的類沒有初始化,則初始化。
8)當一個接口中定義了JDK 8新加入的默認方法(被default關鍵字修飾的接口方法)時,若是有這個接口的實現類發生了初始化,那該接口要在其以前被初始化。
除了上述所講的八種狀況,其餘使用Java類的方式都被看做是類的被動使用,都不會致使類的初始化。
另外,要特別說明的一點
:
接口的加載過程與類加載過程會有所不一樣,接口不能使用 「static{}」語句塊,可是編譯器會爲接口生成對應的
主動使用的第5種:當子類初始化時,要求其父類也要所有初始化完成。可是,對於一個接口的初始化時,並不要求其父接口要所有初始化完成,只有在真正使用到父接口時(好比引用接口中定義的常量)時纔會去初始化,有點延遲加載的意思。
被動使用示例:
1)經過子類引用父類的靜態字段,不會致使子類的初始化
public class Parent { static { System.out.println("Parent init...."); } public static int a = 123; } public class Child extends Parent { static { System.out.println("Child init..."); } } // Test類打印,子類直接調用父類的靜態字段 public static void main(String[] args) { System.out.println(Child.a); }
輸出結果:
Parent init.... 123
根據輸出結果看到,不會輸出 Child init...,經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化,對於靜態字段,只有直接定義這個字段的類纔會被初始化。
2) 建立數組類對象,並不會致使引用的類初始化
public class Child extends Parent { static { System.out.println("Child init..."); } } // 使用 Child 引用建立個數組 public static void main(String[] args) { Child[] child = new Child[1]; System.out.println(child); }
輸出結果:
[Lcom.dskj.jvm.beidong.Child;@7852e922
並無輸出Child init...證實並無初始化com.dskj.jvm.beidong.Child類,根據輸出結果看到了[Lcom.dskj.jvm.beidong.Child
,帶了[L
說明觸發了數組類的初始化階段,它是由JVM自動生成的,繼承自java.lang.Object類,因爲anewarray
助記符觸發建立動做的。
對於數組來講,JavaDoc一般將其所構成的元素稱做爲Component,實際上就是將數組下降一個維度的類型。
助記符:
anewarray:表示建立一個引用類型的(如類、接口、數組)數組,並將其引用值壓入棧頂。
newarray:表示建立一個指定的原始類型的(如int、float、char、short、double、boolean、byte)的數組,並將其引用值壓入棧頂。
對應字節碼內容:
3)調用ClassLoader的loadClass()方法,不會致使類的初始化。
代碼以下:
public class LoadClassTest { public static void main(String[] args) { try { ClassLoader.getSystemClassLoader().loadClass("com.dskj.jvm.passivemode.LoadClass"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } class LoadClass { public static final String STR = "Hello World"; static { System.out.println("LoadClass init..."); } }
沒有輸出 LoadClass init...,證實了調用系統類加載器的loadClass()方法,並不會初始化LoadClass類,由於ClassLoader#loadClass()方法內部傳入的resolve參數爲false,表示Class不會進入到鏈接
階段,也就不會致使類的初始化。
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { ... if (resolve) { //** Links the specified class** resolveClass(c); } }
4)final修飾的常量,編譯時會存入調用類常量池中,本質上沒有引用到定義常量的類,不會致使類的初始化動做。
看下面代碼:
public class ConstClassTest { public static void main(String[] args) { System.out.println(ConstClass.STR); } } class ConstClass { static { System.out.println("ConstClass init..."); } public static final String STR = "Hello World"; }
輸出結果:
Hello World
結果只會輸出 Hello World,不會輸出ConstClass init...,ConstClassTest類對常量ConstClass.STR的引用,實際被轉化爲ConstClassTest類對自身常量池的引用了。也就是說,實際上ConstClassTest的Class文件之中並無ConstClass類的符號引用入口。
編譯完成,兩個ConstClassTest和ConstClass就沒有任何關係了。這句話如何能證實一下?
你能夠先運行一次,而後將編譯後的ConstClass.class文件從磁盤上刪除掉,再次運行跟上面輸出結果是同樣的。
還不信?以下圖所示Idea中的運行結果:
在IDEA下測試時,若是你使用的Gradle來構建,模擬上面的刪除class文件過程,要使用 xxx/out/production/ 目錄下生成編譯後的class文件,當類沒有發生變化時不會從新生成class文件。若是使用默認的 xxx/build/xx,每次運行都會從新生成新的class文件。
若是有問題,能夠在 Project Settings -> Modules -> 項目的 Paths 中調整編譯輸出目錄。
咱們繼續在這個示例基礎上作修改:
public class ConstClassTest { public static void main(String[] args) { System.out.println(ConstClass.STR); } } class ConstClass { // STR 定義的常量經過UUID生成一個隨機串 public static final String STR = "Hello World" + UUID.randomUUID(); static { System.out.println("ConstClass init..."); } }
注意,這裏 STR 常量經過UUID生成一個隨機串,編譯是經過的。
直接運行,輸出結果:
ConstClass init... Hello World:d26d7f1d-2d46-41cb-b5dc-2b7b3fe61e74
看到了ConstClass init...,說明ConstClass類被初始化了。
將ConstClass.class文件刪除後,再次運行:
Exception in thread "main" java.lang.NoClassDefFoundError: com/dskj/jvm/passivemode/ConstClass at com.dskj.jvm.passivemode.ConstClassTest.main(ConstClassTest.java:7) Caused by: java.lang.ClassNotFoundException: com.dskj.jvm.passivemode.ConstClass at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 1 more
你們看到了嗎?ConstClass.class文件被刪除後,再次運行就發生了 java.lang.NoClassDefFoundError
異常了,爲何?正是由於 ConstClass 類裏定義的STR常量並不是編譯器可以肯定的值,那麼其值就不會被放到調用類的常量池中。
這個示例能夠好好理解下,同時印證了該類的初始化時機中,主動使用和被動使用的場景。
你們記住一個類的8種主動使用狀況,都是在開發過程當中常見的使用方式。另外,注意下被動使用的幾種狀況,結合上面的列舉的代碼示例透徹理解。
類加載全過程的每個階段,結合前文給出的圖示,詳細展開。
前面提到的類文件,就是後綴文件爲.class
的二進制文件。
#### JVM在加載階段主要完成以下三件事
1)經過一個類的全限定名,即包名+類名
來獲取定義此類的二進制字節流。
2)將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
3)JVM內存中生成一個表明該類的java.lang.Class對象
,做爲方法區這個類的各類數據的訪問入口。
對於第一點來講,並無要求這個二進制字節流,具體以什麼樣的方式從Class文件中讀取。
經過下面一張圖來彙總一下:
解釋下比較常見的Class文件讀取方式:
1)從ZIP包中讀取Class文件,流行的SpringBoot/SpringCoud框架基本都打成Jar包形式,內嵌了Tomcat,俗稱Fat Jar
,經過java -jar能夠直接啓動,很是方便。
另外,還有一些項目仍然是使用War包形式,而且使用單獨使用Tomcat這類應用容器來部署的。
2)運行時生成的Class文件,應用最多的就是動態代理技術了,好比CGLIB、JDK動態代理。
思考個問題,這些Class文件是由誰來加載的呢?
實現這個動做的代碼正是類加載器來完成的,類加載器在類層次劃分、OSGi、程序熱部署、代碼加密等領域大放異彩,成爲Java技術體系中一塊重要的基石。
對於任意一個類,如何肯定在JVM當中的惟一性?必須是由加載該類的類加載器和該類自己一塊兒共同確立在JVM中的惟一性。
每個類加載器,都擁有一個獨立的類名稱空間。通俗理解:比較兩個類是否『相等』,這兩個類只有在同一個類加載器加載的前提下才有意義。不然,即便這兩個類來源於同一個Class文件,被同一個JVM加載,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。
類加載器之間是什麼關係?
以下圖所示,三種加載器之間的層次關係被稱爲類加載器的 『雙親委派模型(Parents Delegation Model)』。
雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應有本身的父類加載器。不過這裏類加載器之間的父子關係通常不是以繼承(Inheritance)的關係來實現的,而是一般使用組合(Composition)關係來複用父加載器的代碼。圖:
這裏說個有意思的問題,不止一次在某些文章留言中看到糾結:『爲何叫作雙親?』國外文章寫的 parent delegation model,這裏的parent不是單親嗎??應該翻譯爲單親委派模型纔對,全互聯網都跟着錯誤走。。。其實parent這個英文單詞翻譯過來也有雙親的意思,不須要作個『槓精』,沒啥意義哈。
結合類加載器的自底向上的委託關係總結:
假設一個類處於ClassPath下,版本是JDK8,默認使用應用類加載器進行加載。
1)當應用類加載器收到了類加載的請求,會把這個請求委派給它的父類(擴展類)加載器去完成。
2)擴展類加載器收到類加載的請求,會把這個請求委派給它的父類(引導類)加載器去完成。
3)引導類加載器收到類加載的請求,查找下本身的特定庫是否能加載該類,即在rt.jar、tools.jar...包中的類。發現不能呀!返回給擴展類加載器結果。
4)擴展類加載器收到返回結果,查找下本身的擴展目錄下是否能加載該類,發現不能啊!返回給應用類加載器結果。
5)應用類加載器收到結果,額!都沒有加載成功,那隻能本身加載這個類了,發如今ClassPath中找到了,加載成功。
你對併發很感興趣,本身建立了個跟JDK同樣的全限定名類LongAdder, java.util.concurrent.atomic.LongAdder
,而後程序啓動交給類加載器去加載,能成功嗎?
固然不能!這個LongAdder是 Doug Lea 大神寫的,貢獻到JDK併發包下的,而且被安排在rt.jar包中了,所以是由 Bootstrap ClassLoader 類加載器優先加載的,別人誰寫一樣的類,那就是故意跟JDK做對,是絕對不允許的。
即便你寫了一樣的類,編譯能夠經過,可是永遠不會被加載運行,被JDK直接忽略掉。
雙親委派模型在JDK中內部是如何實現的?
JDK中提供了一個抽象的類加載器 ClassLoader,其中提供了三個很是核心的方法。
public abstract class ClassLoader { //每一個類加載器都有個父加載器 private final ClassLoader parent; public Class<?> loadClass(String name) { //查找一下這個類是否是已經加載過了 Class<?> c = findLoadedClass(name); //若是沒有加載過 if( c == null ){ //先委託給父加載器去加載,注意這是個遞歸調用 if (parent != null) { c = parent.loadClass(name); }else { // 若是父加載器爲空,查找Bootstrap加載器是否是加載過了 c = findBootstrapClassOrNull(name); } } // 若是父加載器沒加載成功,調用本身的findClass去加載 if (c == null) { c = findClass(name); } return c; } protected Class<?> findClass(String name){ //1. 根據傳入的類名name,到在特定目錄下去尋找類文件,把.class文件讀入內存 ... //2. 調用defineClass將字節數組轉成Class對象 return defineClass(buf, off, len); } // 將字節碼數組解析成一個Class對象,用native方法實現 protected final Class<?> defineClass(byte[] b, int off, int len){ ... } }
參見ClassLoader核心代碼註釋,提取和印證幾個關鍵信息:
1)JVM 的類加載器是分層次的,它們有父子關係,每一個類加載器都有個父加載器,是parent字段。
2)loadClass() 方法是 public 修飾的,說明它纔是對外提供服務的接口。根據源碼可看出這是一個遞歸調用,父子關係是一種組合關係,子加載器持有父加載器的引用,當一個類加載器須要加載一個 Java 類時,會先委託父加載器去加載,而後父加載器在本身的加載路徑中搜索 Java 類,當父加載器在本身的加載範圍內找不到時,纔會交還給子加載器加載,這就是所謂的『雙親委託模型』。
3)findClass() 方法的主要職責就是找到 .class 文件,可能來自磁盤或者網絡,找到後把.class文件讀到內存獲得byte[]字節碼數組,而後調用 defineClass() 方法獲得 Class 對象。
4)defineClass() 是個工具方法,它的職責是調用 native 方法把 Java 類的字節碼解析成一個 Class 對象,所謂的 native 方法就是由 C 語言實現的方法,Java 經過 JNI 機制調用。
JDK8以及以前的JDK版本都是以下三層類加載器實現方式。
1)啓動類加載器(Bootstrap ClassLoader),這個類加載器是由C++實現的,負載加載$JAVA_HOME/jre/lib目錄下的jar文件,好比 rt.jar、tools.jar,或者-Xbootclasspath系統環境變量指定目錄下的路徑。它是個超級公民,即便開啓了Security Manager的時候,它也能擁有加載程序的全部權限,使用null做爲擴展類加載器的父類。
同時,啓動類加載器在JVM啓動後也用於加載擴展類加載器和系統類加載器。
2)擴展類加載器(Extension ClassLoader),這個類加載器由sun.misc.Launcher$ExtClassLoader
來實現,負責加載$JAVA_HOME/jre/lib/ext目錄中,或者java.ext.dirs系統變量指定路徑中全部的類庫,容許用戶將具有通用性的類庫能夠放到ext目錄下,擴展Java SE功能。在JDK 9以後,這種擴展機制被模塊化帶來的自然的擴展能力所取代。
3)應用類加載器(App/System ClassLoader),也稱做爲系統類加載器,這個類加載器由sun.misc.Launcher$AppClassLoader
來實現。 它負責加載用戶應用類路徑(ClassPath)上全部的類庫,開發者一樣能夠直接在代碼中使用這個類加載器。若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
##### JDK9中的類加載器有哪些變化?
1)擴展類加載器被重命名爲平臺類加載器(Platform ClassLoader),部分不須要 AllPermission 的 Java 基礎模塊,被降級到平臺類加載器中,相應的權限也被更精細粒度地限制起來。
2) 擴展類加載器機制被移除。這會帶來什麼影響呢?就是說若是咱們指定 java.ext.dirs 環境變量,或者 $JAVA_HOME/jre/lib/ext目錄存在,JVM會返回錯誤。 建議解決辦法就是將其放入 classpath 裏。部分不須要 AllPermission 的 Java 基礎模塊,被降級到平臺類加載器中,相應的權限也被更精細粒度地限制起來。
3)在$JAVA_HOME/jre/lib路徑下的 rt.jar 和 tools.jar 一樣是被移除了。JDK 的核心類庫以及相關資源,被存儲在 jimage 文件中,並經過新的 JRT 文件系統訪問,而不是原有的 JAR 文件系統。
4)增長了 Layer 的抽象, JVM 啓動默認建立 BootLayer,開發者也能夠本身去定義和實例化 Layer,能夠更加方便的實現相似容器通常的邏輯抽象。
新增的Layer的抽象,去內部的BootLayer做爲內建類加載器,包括了 BootStrap Loader、Platform Loader、Application Loader,其餘 Layer 內部有自定義的類加載器,不一樣版本模塊能夠同時工做在不一樣的 Layer。
結合了 Layer,目前最新的 JVM 內部結構以下圖所示:
由於JDK裏的類加載器ClassLoader是抽象類,若是你自定義類加載器能夠重寫 findClass() 方法,重寫 findClass() 方法仍是會按照既定的雙親委派機制運做的。
而咱們發現loadClass()方法也是public修飾的,說明也是容許重寫的,重寫loadClass()方法就能夠『隨心所欲』了,不按照既定套路出牌了,不遵循雙親委派模型。
典型的就是Tomcat應用容器,就是自定義WebAppClassLoader類加載器,打破了雙親委派模型。
WebAppClassLoader 類加載器具體實現是重寫了 ClassLoader 的兩個方法:loadClass() 和 findClass()。其大體工做過程:首先類加載器本身嘗試去加載某個類,若是找不到再委託代理給父類加載器,其目的是優先加載 Web 應用本身定義的類。
這也正是一個Tomcat可以部署多個應用實例的根本緣由。
接下來,咱們分析下源碼實現:
loadClass() 重寫方法的源碼實現,僅保留最核心的代碼便於理解:
// 重寫了 loadClass() 方法 public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 使用了synchronized同步鎖 synchronized (getClassLoadingLock(name)) { Class<?> clazz = null; //1)先在本地緩存中,查找該類是否已經加載過 clazz = findLoadedClass0(name); if (clazz != null) { if (resolve) // 本地緩存找到,鏈接該類 resolveClass(clazz); return clazz; } //2) 從系統類加載器的緩存中,查找該類是否已經加載過 clazz = findLoadedClass(name); if (clazz != null) { if (resolve) // 從系統類加載器緩存找到,鏈接該類 resolveClass(clazz); return clazz; } // 3)嘗試用ExtClassLoader類加載器類加載 ClassLoader javaseLoader = getJavaseClassLoader(); try { clazz = javaseLoader.loadClass(name); if (clazz != null) { if (resolve) // 從擴展類加載器中找到,鏈接該類 resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } // 4)嘗試在本地目錄查找加載該類 try { clazz = findClass(name); if (clazz != null) { if (resolve) // 從本地目錄找到,鏈接該類 resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } // 5) 嘗試用系統類加載器來加載 try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (resolve) // 從系統類加載器中找到,鏈接該類 resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } //6. 上述過程都加載失敗,拋出異常 throw new ClassNotFoundException(name); }
loadClass() 重寫的方法實現上會複雜些,畢竟打破雙親委派機制就在這裏實現的。
主要有以下幾個步驟:
1)先在本地緩存 Cache 查找該類是否已經加載過,即 Tomcat 自定義類加載器 WebAppClassLoader 是否已加載過。
2)若是 Tomcat 類加載器沒有加載過這個類,再看看系統類加載器是否加載過。
3)若是系統類加載器也沒有加載過,此時,會讓 ExtClassLoader 擴展類加載器去加載,很關鍵,其目的防止 Web 應用本身的類覆蓋 JRE 的核心類。
由於 Tomcat 須要打破雙親委託機制,假如 Web 應用裏有相似上面舉的例子自定義了 Object 類,若是先加載這些JDK中已有的類,會致使覆蓋掉JDK裏面的那個 Object 類。
這就是爲何 Tomcat 的類加載器會優先嚐試用 ExtClassLoader 去加載,由於 ExtClassLoader 會委託給 BootstrapClassLoader 去加載,JRE裏的類由BootstrapClassLoader安全加載,而後返回給 Tomcat 的類加載器。
這樣 Tomcat 的類加載器就不會去加載 Web 應用下的 Object 類了,也就避免了覆蓋 JRE 核心類的問題。
4)若是 ExtClassLoader 加載器加載失敗,也就是說 JRE 核心類中沒有這類,那麼就在本地 Web 應用目錄下查找並加載。
5)若是本地目錄下沒有這個類,說明不是 Web 應用本身定義的類,那麼由系統類加載器去加載。這裏請你注意:Web 應用是經過Class.forName調用交給系統類加載器的,由於Class.forName的默認加載器就是系統類加載器。
6)若是上述加載過程所有失敗,拋出 ClassNotFoundException 異常。
findClass() 重寫方法的源碼實現,僅展現最核心代碼便於理解:
// 重寫了 findClass 方法 public Class<?> findClass(String name) throws ClassNotFoundException { ... Class<?> clazz = null; try { //1) 優先在本身Web應用目錄下查找類 clazz = findClassInternal(name); } catch (RuntimeException e) { throw e; } if (clazz == null) { try { //2) 若是在本地目錄沒有找到當前類,則委託代理給父加載器去查找 clazz = super.findClass(name); } catch (RuntimeException e) { throw e; } //3) 若是父類加載器也沒找到,則拋出ClassNotFoundException if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }
在 findClass() 重寫的方法裏,主要有三個步驟:
1)先在 Web 應用本地目錄下查找要加載的類。
2)若是沒有找到,交給父加載器去查找,它的父加載器就是上面提到的系統類加載器 AppClassLoader。
3)如何父加載器也沒找到這個類,拋出 ClassNotFoundException 異常。
咱們都知道Jdbc是一個標準,那麼具體數據庫廠商會根據Jdbc標準提供本身的數據庫實現,既然Jdbc是一個標準,這些類原生的會存在JDK中了,好比Connection、Statement,並且是位於rt.jar包中的,他們在啓動的時候是由BootstrapClassLoader加載的。
那麼怎麼具體加載廠商的實現呢?
確定是經過廠商提供相應的jar包,而後放到咱們應用的ClassPath下,這樣的話,廠商所提供的jar中的確定不是由啓動類加載器去加載的。
因此,廠商的具體驅動的實現是由應用類加載器進行加載的 。
Connection是一個接口,它是由啓動類加載器加載的,而它具體的實現啓動類加載器沒法加載,由系統類加載器加載的。這樣會存在什麼樣的問題?
根據類加載原則:
SPI(Service Provider Interface)
父ClassLoader可使用當前線程Thread.currentThread().getContextClassLoader()所指定的classloader加載的類。
這就改變了父ClassLoader不能使用子ClassLoader或是其餘沒有直接父子關係的ClassLoader所加載類的狀況,即改變了雙親委託模型。
線程上下文類加載器就是當前線程的Current Classloader。
在雙親委託模型下,類加載器是由下而上,即下層的類加載器會委託上層進行加載。可是對於SPI來講,有些接口是Java核心庫所提供的,而Java核心庫是由啓動類加載器來加載的,而這些接口的實現卻來自於不一樣jar包(廠商提供),Java的啓動類加載器是不會加載其餘來源的jar包,這樣傳統的雙親委託模型就沒法知足SPI的要求。
而經過給當前線程設置上下文類加載器,就能夠由設置的上下文類加載器來實現對於接口實現類的加載。
線程上下文類加載器的通常使用模式:
獲取 ---> 使用 --> 還原
ClassLoader classloader = Thread.currentThread().getContextClassLoader(); try { // 將目標類加載器設置到上下文類加載器 Thread.currentThread().setContextClassLoader(targetTccl); // 在該方法中使用設置的上下文類加載器加載所需的類 doSomethingUsingContextClassLoader(); } finally { // 將原來的classloader設置到上下文類加載器 Thread.currentThread().setContextClassLoader(classloader); }
doSomethingUsingContextClassLoader()方法中則調用了 Thread.currentThread().getContextClassLoader() ,獲取當前線程的上下文類加載器作某些事情。
若是一個類由類加載器A加載,那麼這個類的依賴類也是由相同的類加載器加載的(若是該依賴類以前沒有被加載過的話)。
在SPI的接口代碼當中,就能夠經過上下文類加載器成功的加載到SPI的實現類。所以,上下文類加載器在不少的SPI的實現中都會獲得大量的應用。
當高層提供了統一的接口讓低層(好比Jdbc各個廠商提供的具體實現類)去實現,同時又要在高層加載(或實例化)低層的類時,就必需要經過線程上下文類加載器來幫助高層的類加載器並加載該類(本質上,高層的類加載器與低層的類加載器是不同的)
通常狀況下,咱們沒有修改過線程上下文類加載器,默認的就是系統類加載器。因爲是運行期間是設置的上下文類加載器,因此,無論當前程序在什麼地方,在啓動類的加載器的範圍內仍是擴展類加載器的範圍內,那麼咱們在任何有須要的時候都是能夠經過Thread.currentThread().getContextClassLoader()獲取設置的上下文類加載器來完成操做。
這個也有點像ThreadLocal的類,若是藉助於ThreadLocal的話就沒有必要同步,由於每個線程都有相應的數據副本,這些數據副本之間是互不干擾的,他們只能被當前的線程所使用和訪問,既然每一個線程都有數據副本,每一個線程固然操做的是副本,因此線程之間就不須要同步、鎖就能夠處理併發。ThreadLocal本質上是用空間換時間的概念,由於咱們將數據拷貝多份會佔用必定的內存空間,每一個線程中去使用。
限於篇幅,本文主要對類的初始化時機,類的加載過程當中最重要的類加載器機制進行了分析,對其中的雙親委派模型,以及Tomcat是如何打破雙親委派模型的,結合源代碼進行了深刻剖析,對上下文類加載器是如何改變雙親委派模型進行了分析。
總結一下:
一個類都是經過主動使用
的方式加載到JVM當中的,到目前爲止一共總結了八種狀況,除此以外的都屬於被動使用
,被動使用的列舉了代碼示例,結合示例能夠更爲清晰的理解。
詳細介紹了雙親委派模型的工做過程,JDK8和JDK9版本中類加載器層次關係,類加載器的結果本質上並非一種樹形結構,而是一種包含關係。
同時,也介紹了Tomcat是如何打破雙親委派機制的,經過源碼透視打破規則的全過程。
最後,對上下文類加載器根據Jdbc的例子,進一步分析了使用模式,如何改變雙親委派機制作到父類加載器,能夠加載和使用各個廠商提供的實現類的。
另外,回到最初的圖示,一個類要想順利進入到JVM內存結構中,除了類的加載階段外,還有驗證、準備、解析、初始化四個階段完成後,纔算真正完成類的初始化操做。
在JVM中某個類的Class對象再也不被引用,即不可觸及,Class對象就會結束生命週期,該類在方法區內的數據會被卸載,從而技術該類的整個生命週期。
一個類什麼時候結束生命週期,取決於表明它的Class對象什麼時候結束生命週期。
可是,JVM自帶的類加載器所加載的類,在虛擬機的生命週期中,始終不會被卸載。前面已經介紹過,JVM自帶的類加載器包括引導類加載器、擴展類加載器和系統類加載器(應用類加載器)。Java虛擬機自己會始終引用這些類加載器,而這些類加載器會始終引用它們所加載的類的Class對象,所以這些Class對象是始終可觸及的。
在以下狀況下,JVM將結束生命週期。
執行了System.exit()
程序正常執行結束
程序在執行過程當中遇到了異常或者錯誤而異常終止
因爲操做系統出現錯誤而致使Java虛擬機進程終止
你們如何以爲本文有收穫關個注唄,碼字不易,文章不妥之處,歡迎留言斧正。本號不按期會發布精彩原創文章。
參考資料:
深刻理解Java虛擬機
極客時間課程
歡迎關注個人公衆號,掃二維碼關注得到更多精彩文章,與你一同成長~