在Java內存區域介紹, 及垃圾收集中都有提到過, 方法區這個概念, 存儲的是Java的類信息, 當Java類被加載以後, 就會被存儲到方法區中。html
那麼Java類是如何被加載的呢?Jvm又是如何解讀 class 文件, 全限定名等等相關的東西又是怎樣融入Java的體系中呢?java
這裏的class文件並不是僅僅是指 .class文件, 而是符合Java虛擬機規範的 class文件或相應的字節流。甚至於 class文件並不是與Java強相關, 只要可以被相應的編譯器解讀並生成相應的 class文件,在這裏並不在意它的語言來源到底是什麼。windows
並不是僅僅是class文件有這種特性,對於計算機而言,自己也是如此,只要可以轉換爲相應的寄存器指令,並不在意,也不須要管指令的生成來源是什麼,而Jvm虛擬機正是解讀字節碼指令的平臺。數組
class文件是一組以8位字節爲基礎單位的二進制流,因此class文件的數據存儲是按照嚴格的規則進行存儲,按照順序解讀,每一個字節,每一個位置表明什麼都是被嚴格限定的,它沒有相關的描述符, 只有這樣纔可以被jvm解讀。安全
class文件中只有兩種數據結構:網絡
無符號數數據結構
無符號數屬於基本數據類型,以u1,u2,u4,u8分別來表明:1,2,4,8個字節的無符號數。多線程
無符號數能夠用來描述:數字,索引引用,數量值或按照UTF-8編碼構成的字符串值。框架
換句話說,就是:數值,能夠被用來作相關運算的數值;索引;表示表大小,屬性長度等等的數量值;以及存儲的字面量。 包括 變量名, 字符串常量等其餘值。jvm
表
表, 是有層次關係的複合數據結構。
class文件的解讀真的是沒有什麼奧祕可言,均是人爲規定了 在某個特定的位置表示什麼意義,該如何被解讀, 當拿到一個class文件只須要按照Java虛擬機規範中一點點解讀,就可以將整個class文件翻譯成可讀懂的文件, 甚至於翻譯成Java代碼也不是問題。
eg. 每一個class文件的頭四個字節都是0xCAFFBABA,這個魔數值用來表示當前文件是能夠被JVM解讀的class文件,也是文件類型的真正描述,畢竟咱們都知道後綴名能夠被隨意更改而不影響文件使用特定的方式進行解讀。
eg. 緊接着的魔數值得四個字節存儲的class文件的版本號, 前兩個是次版本號,後兩個是主版本號。 主版本號時從45開始,而每向上一個主版本則加一。
JVM虛擬機能夠解讀更低版本的class文件,而對高版本的class文件拒絕解讀。
而剩餘的class文件解讀方式都是諸如此類的方式。
固然, 咱們不須要本身費勁的去將class文件轉換爲16進制,而後一個個去解讀它。(windows中以16進制的方式顯示 能夠採用 WinHex軟件)
在Java環境下,咱們僅僅須要
javap -verbose <classpath>
便可,但僅可以輸出public 方法,如果想知道更全面的信息
javap -h
有相關的提示信息,其使用語法則是:
javap <options> <classes>
class文件中的常量池,之因此要提到這個概念,與以後要提到的一個概念:動態鏈接,息息相關, 所以須要略作介紹。
直接看代碼:
public class TestClass{ public int a = 1; public static final String b = "zyzdisciple"; public String myName = "zyzdisciple2"; public static int re(int a, final int b) { return a + b; } }
通過編譯後, javap -verbose TestClass.class,截取其常量池片斷:
Constant pool: #1 = Methodref #6.#22 // java/lang/Object."<init>":( #2 = Fieldref #5.#23 // TestClass.a:I #3 = String #24 // zyzdisciple2 #4 = Fieldref #5.#25 // TestClass.myName:Ljava/lang #5 = Class #26 // TestClass #6 = Class #27 // java/lang/Object #7 = Utf8 a #8 = Utf8 I #9 = Utf8 b #10 = Utf8 Ljava/lang/String; #11 = Utf8 ConstantValue #12 = String #28 // zyzdisciple #13 = Utf8 myName #14 = Utf8 <init> #15 = Utf8 ()V #16 = Utf8 Code #17 = Utf8 LineNumberTable #18 = Utf8 re #19 = Utf8 (II)I #20 = Utf8 SourceFile #21 = Utf8 TestClass.java #22 = NameAndType #14:#15 // "<init>":()V #23 = NameAndType #7:#8 // a:I #24 = Utf8 zyzdisciple2 #25 = NameAndType #13:#10 // myName:Ljava/lang/String; #26 = Utf8 TestClass #27 = Utf8 java/lang/Object #28 = Utf8 zyzdisciple
在常量池中主要存儲兩大類型常量: 字面量 和 符號引用:
字面量: 文本字符串, 聲明爲final的常量值。
如上例子中的 #28 存儲的就是常量, 同時也做爲字面量 其中的 String, #12 #3 都是字面量。
符號引用:
符號引用包括下面三類常量:
類和接口的全限定名; #5 #6(TestClass繼承自Object, 父類的全限定名也存儲爲符號引用。) 在常量池中存儲的類全限定名有如下幾種:
目前我試過且能想到的只有這幾種。
方法的名稱及描述符
字段的名稱很少解釋, 而這裏的描述符則是指用來描述字段的數據類型,方法的參數列表(包括數量,類型,及順序)及返回值。
其描述符與類型的關係對應以下:
B byte; C char; D double; F float; I int; J long;
S short; Z boolean; V void; L 對象類型 eg. Ljava/langg/Object
正是經過符號引用, 來指向對應的常量,從上述也能夠看出來, 對於class而言,方法的返回值類型不一樣,其實應該表示的是不一樣的方法,由於其描述符不一樣。
但在編譯器中卻不容許這種狀況出現,不只不能重載, 也不能進行重寫。都會提示錯誤。
既然Java文件編譯以後生成的class文件存儲有全部相關信息,那麼我寫的那麼一堆代碼呢? 那一堆if else for 循環都到哪裏去了?
答案就是存儲在code屬性中。
經過 javap命令就能夠看到相關的數據。
java程序方法體中的的代碼通過編譯器處理後,最終變爲字節碼指令存儲在code屬性內, code_length用來表明字節碼長度, 而code屬性則用來存儲一系列的字節碼流。 code_length雖然是一個 u4長度的值,理論上最大能夠到達 2^32 - 1的長度, 但事實上,在java虛擬機規範中明確要求不可以超過65535條字節碼指令,也即U2的長度, 超過即會致使編譯失敗。
而這種問題最可能出如今 jsp頁面的編寫中。
一樣,有代碼以下:
public class TestA extends TestB { public int add (int a, int b) { return a + b; } } Code: stack=2, locals=3, args_size=3 0: iload_1 1: iload_2 2: iadd 3: ireturn
code屬性就是上面那樣的一條條字節碼指令, 字節碼指令正是由u1類型的, 表明着某特種特定操做含義的數字(稱爲操做碼)以及跟隨其後的零至多個表明此操做所需的參數構成的。
每一個字節碼指令都是一個u1類型的單字節, 也就意味着在 java中最多能夠表達256種指令。
而java中的大部分指令都是不含操做數的。這點就與基於寄存器的指令集有所不一樣。
以前在java內存中提到過,java的操做是基於棧的,最小的操做元素是棧幀,也即一個 slot。
而在上面的代碼中也明確看到了: 對全部指令都沒有明確的指明操做數是誰,由於所作的操做無非只有兩種,出棧,入棧。
而相應的當肯定了指令是誰, 其表明的含義便是對當前棧頂元素進行的操做,棧頂元素也就天然而然的成爲了相應的操做數。
而字節碼指令自己大都也已經指定了相應的操做數類型到底是什麼了。
字節碼指令按照功能主要有如下幾類:
加載和存儲指令
即將數據在棧幀中的局部變量表和操做數棧之間來回傳輸。
運算指令
用於對兩個操做數棧上的值進行某種特定運算,並把結果從新存儲在操做數棧頂。
類型轉換指令
能夠將兩種不一樣的數值類型進行轉換。
對象的建立與訪問指令
操做數棧管理指令
控制轉移指令
即各類 判斷 跳轉指令
方法調用和返回指令
異常處理指令
同步指令
即synchronizedx相關。
終於, 咱們在Class文件中存儲了種種信息,那麼虛擬機又如何,在什麼時候,經過怎樣的方式將class文件加載,將信息存入內存中呢?
類從被加載到虛擬機內存中開始,到被卸載爲止,共經歷這樣幾個階段:
加載, 驗證, 準備, 解析, 初始化, 使用, 卸載7個階段。
其中前五個階段是按序開始, 但僅僅是指開始,並無說按序進行,能夠交叉進行。
對於什麼時候加載一個類,虛擬機中沒有明確的規範, 但對什麼時候必須初始化一個類, 倒是有着很是明確且嚴格的約定, 「有且只有」如下五種狀況:
遇到new、putstatic、getstatic、invokestatic、 這四條字節碼指令時,若是類沒有進行初始化, 須要先觸發其初始化。 這是指, new 一個實例對象時, 讀取或設置一個類的靜態字段時,以及調用一個類的靜態方法時。
使用java.lang.reflect包的方法對類進行反射調用時。
當初始化一個類時, 若是發現其父類尚未進行初始化, 則需觸發其父類的初始化。(但接口並不會進行相應的初始化)
當虛擬機啓動時,用戶須要指定一個要執行的主類(main()入口類),虛擬機會先初始化這個類。
當使用JDK1.5支持時,若是一個java.langl.incoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。
而除了上述五種之外, 全部的引用類方法都不會觸發初始化, 稱爲被動引用。
須要注意的是:
MyObject obj = new MyObject[]
這種代碼並不會觸發MyObject的類的初始化,數組的建立指令是 newarray, 並不在上述的五種狀況中。
而接口也一樣不會進行初始化, 只有當使用到其中常量時纔會進行初始化。
加載是類加載的第一個階段, 在加載階段:虛擬機須要完成:
經過一個類的全限定名來獲取此類的二進制字節流。
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時內存結構。
在內存中生成一個表明這個類的java.lang.Class對象,並做爲方法區中該類的訪問入口。
在加載過程 JVM虛擬機給予了最大的靈活性。
能夠從ZIP包中讀取, 如 jar包, war包
能夠從網絡中讀取
能夠運行時計算生成
能夠從其餘文件生成轉換而來 等其餘方式。
對於運行時自動生成這點而言,運用的最多的地方是動態代理技術,在java.lang.Proxy 中, 就是用了 ProxyGenerator.generatoerProxyClass 來生成特定的「*$Proxy」的代理類的二進制字節流。
至於什麼是動態代理, 參考連接:
文件格式驗證
因爲第一步加載中的 class的來源多種多樣, 所以class文件的安全性就沒法保證, 若是從網絡上加載了一個非法的 class文件,卻沒有進行校驗,可能會致使虛擬的崩潰,所以, 校驗有必定的必要性。
如同大多數校驗同樣, 當咱們拿到一個.class文件時, 首先校驗的是, 這是不是一個class文件,可否正確識別這是一個class文件, 又因爲class文件的格式規則有着嚴格要求, 它是否知足?
凡此種種,其目的只是爲了可以將class文件正確解析,並能夠按照規則存入方法區, 而在這以後,字節流就被加載進入方法區, 其後的校驗均是對方法區的存儲結構校驗。
元數據驗證
在文件格式知足要求且能夠正確存入方法區以後,第二步所須要作的事情就是校驗是否符合java語言的基礎規範自己。
在class文件中存儲有相應的字段描述符, class繼承關係等等, 但僅僅是存儲, 並不會校驗是否合理, 如在一個 class中引用了另外一個class的private 字段也是依然能夠的。
字節碼驗證
保證了數據符合java的語法規範以後, 下一步要作的是驗證語義是否表達完整, 符合規範。元數據驗證更多的是驗證是否可進行相應的操做,根據java的關鍵字, 等基礎規則驗證是否可行, 而不進行實際上的語義判斷。
符號引用驗證
這個階段的校驗發生在【解析】階段,是在將虛擬機的符號引用轉換爲直接引用的時候,符號引用驗證能夠看作是對類自身之外的(常量池中的各類符號引用)信息進行匹配性校驗,一般需校驗:
根據全限定名可否找到對應的類
在指定類中是否存在符合須要的方法,字段
符號引用中的類、字段、方法的訪問性是否可以被當前類訪問。
準備階段是爲類變量(static)分配內存並設置初始值的階段。
這裏的分配內存並不是是指分配堆內存, 而是將變量的索引存入方法區, 若是是基礎變量的話, 也會存入相應的值, 對於:
private static Object obj = new Object();
而言,new Object()所佔用的內存依然是在堆中, 不難理解,當再度爲static變量賦值爲其餘對象時, 原有的Object的內存就能夠被回收, 所以充分知足 一個對象的相關回收條件。
但若是類字段爲 final 的, 在準備階段還會對類變量進行賦值。
是將虛擬機常量池中的符號引用 替換爲 直接引用的過程。
在以前提到過符號引用有三種, 類的接口及全限定名, 字段名稱及描述符, 方法名稱及描述符。
而直接引用則是直接指向目標的指針、相對偏移量、或是一個可以間接定位到目標的句柄。
虛擬機規範中並未規定解析階段發生的具體時間,只要求在執行了newarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevritual,ldc,ldc_w, multianewarray,new,putfield和putstatic這16個用於操做符號引用的字節碼指令以前,先對其符號引用進行解析。
解析主要針對如下幾種:
類或接口、字段、類方法、接口方法、方法類型、方法句柄、點限定符
類或接口的解析
假設當前所處的類爲D, 若是要把一個從未解析過的符號引用N解析爲一個類或接口C的直接引用, 須要經歷如下步驟:
a. 若是C並不是數組類型,那麼虛擬機會將表明N的全限定名傳遞給D的類加載器去加載這個類C,在加載過程當中若是觸發了任何異常都會宣告解析失敗。
b.若是C是一個數組類型,而且數組的元素類型爲對象,也就是N的描述符會是相似」[Ljava.lang.Integer"的形式,那麼會按照第a點的規則加載數組元素類型。若是N的描述符如前面所假設的形式,須要加載的元素類型就是「java.lang.Integer」,接着由虛擬機生成一個表明此數組和元素的數組對象。
c.若是上面的步驟沒有出現任何異常,那麼C在虛擬機中實際上已經成爲一個有效的類或接口了,但在解析完成以前還要進行符號引用驗證,確認C是否具有對D的訪問權限,若是發現不具體訪問權限,將拋出java.lang.IllegalAccessError錯誤。
字段解析
要解析一個未被解析過的字段符號引用,首先將會對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所屬性的類或接口的符號引用。若是在解析這個類或接口符號引用過程當中出現了任何異常,都會致使字段符號引用解析失敗。若是解析成功完成,那麼這個字段所屬性的類或接口用C表示,虛擬機規範要求以下步驟對C進行後續字段的搜索:
a.若是C自己就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
b.不然,若是C中實現了接口,將會按照繼承關係從下往上遞歸搜索各個接口和它的父接口,若是接口中包含了簡單名稱答字段描述符都與目標相匹配的字段,則返回該字段的直接引用,查找結束。
c.不然,若是C不是java.lang.Object的話,將會按照繼承關係從下往上遞歸搜索其父類,若是在父類中包含了簡單名稱和字段描述符都與目標匹配的字段,則返回這個字段的直接引用,查找結束。
d.不然,查找失敗,招拋出java.lang.NoSuchFieldError錯誤。
須要注意的是, 雖然在解釋中 b.c是擁有前後順序的, 可是在實際應用中, 若是一個類 其自身實現了接口(非父類實現接口), 同時在繼承樹,或接口列表中存在相同字段,則是不被容許的。不管接口或是父類都是其直接上級,沒法肯定字段的究竟歸屬是誰。
public class TestB extends TestD implements TestInterface { //public TestC testC = new TestC("objectB"); } public class TestD { public TestC testC = new TestC("objectD"); } public interface TestInterface { TestC testC = new TestC("interface"); } public static void main(String[] args) { TestB testB = new TestB(); //編譯報錯 TestC testC = testB.testC; System.out.println("~~~~~~~~~~~~~~~"); System.out.println(testC.name); }
此時由於TestB中不存在 testC屬性,所以須要向上級查找,沒法獲知到底是誰, 惟有當將註釋放開, 便可經過。
類方法解析
類方法解析的第一個步驟與字段解析同樣,也是須要先解析出方法表的class_index項中索引的方法所屬性類或接口的符號引用,若是解析成功,依然用C表示這個類,接下來虛擬機將會按照以下步驟進行後續的類方法搜索:
a.類方法和接口方法符號引用的常量類型定義是分開的,若是在類方法表中發現了class_index中索引的C是個接口,那麼直接就拋出java.lang.IncompatibleClassChangeError錯誤。
b.若是通了第a步,在類C中查找是否有簡單名稱和描述符與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
c.不然,在類C的父類中遞歸查找是否有簡單名稱和字段描述符都與目標匹配的方法,則返回這個方法的直接引用,查找結束。
d.不然,在類C實現的接口列表及它們的父接口中遞歸查找否有簡單名稱和字段描述符都與目標匹配的方法,說明類C是一個抽象類,這時候查找結束,拋出java.lang.AbstractMethodError錯誤。
e.不然,宣告查找失敗,拋出java.lang.NoSuchMethodError錯誤。
最後,若是查找過程成功返回了直接引用,將會對暈個方法進行權限驗證;若是發現不具務對此方法的訪問權限,將拋出java.lang.IllegalAccessError錯誤。
在這裏困惑了很多時間, 這裏須要注意的一個地方是: 類方法,僅僅是指 static 方法,並不包括相應的實例方法,與前面提到的字段解析有所不一樣,字段解析並無對 是否爲 靜態進行區分。
這點與java的動態語言調用有關, 下一篇再提。
類的初始化是類加載過程的最後一步,前面的類加載動做,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段,才真正執行類中定義的Java程序代碼(或者說是字節碼)。
在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段是執行類構造器<clinit>()方法的過程。
<clinit>()方法是由編譯器自動收集類中全部類變量的賦值動做和靜態語句塊(static{})中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後變量,在前面的靜態語句塊中能夠賦值,可是不能訪問。
<clinit>()方法與類的構造器<init>()不一樣,它不須要顯示地調用父類類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以在虛擬機中第一個被執行<clinit>()方法的類確定是java.lang.Object。
<clinit>()方法對於類或接口來講並非必須的,若是一個類中沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這類生成<clinit>()方法。
接口中不能使用靜態語句塊,但仍然能夠有變量初始化的同仁操做,所以接口與類同樣都會生成<clinit>()方法,但接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量被使用時,父接口才會初始化。
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其它線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有很耗時的操做,那就可能形成多個線程阻塞,在實際應用中這種阻塞每每是很隱蔽的。
在類加載階段, 「經過一個類的全限定名來獲取描述此類的二進制字節流」, 這個動做放在java虛擬機外部去實現, 而實現這個動做的代碼模塊被稱爲「類加載器」。
對於任意一個類, 都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性. 比較兩個類(並不是是指實例對象)是否相等,只有在同一種類加載器下才有意義.
相等,指的是, 類的Class對象的equals方法, isInstance()方法的返回值, 還有 instance of 關鍵字的斷定結果.
而你們都有所耳聞的是, Java的類加載機制採起的是雙親委派模型:
從Java虛擬機的角度來說, 只存在兩種不一樣的類加載器:一是啓動類加載器(Bootstrap ClassLoader), 這個類加載器採用 C++語言實現, 是虛擬機自身的一部分,另外一種則是其餘全部的類加載器. 這些類都繼承自抽象類java.lang.ClassLoader.
從Java開發者的角度來看, 能夠分爲三種類加載器.
a. 啓動類加載器(BootstrapClassLoader) Bootstrap類加載器負責加載rt.jar中的JDK類文件,它是全部類加載器的父加載器.
b. 擴展類加載器(Extension ClassLoader),加載目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件。還能夠加載-D java.ext.dirs選項指定的目錄.
c. 應用程序類加載器(Application ClassLoader), 也即 ClassLoader.getSystemClassLoader()方法的返回值.通常也稱做系統類加載器,它負責加載用戶類路徑上所指定的類庫, 若是應用程序中不曾自定義過類加載器, 那麼通常這個就是程序中默認的類加載器.
一樣的, 執行相同的代碼, 我發現與網上獲得的結論都是不一樣的, 我在這裏用的是jdk11的最後一個版本.
public static void main(String[] args) { System.out.println(Thread.currentThread().getContextClassLoader()); Class<?> cls = Main.class; // 取得Class類對象的類加載器信息 System.out.println(cls.getClassLoader()); // 取得Class類對象的類加載器父加載器信息 System.out.println(cls.getClassLoader().getParent()); // 取得Class類對象的類加載器父加載器的父加載器信息 System.out.println(cls.getClassLoader().getParent().getParent()); }
輸出的是:
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc jdk.internal.loader.ClassLoaders$PlatformClassLoader@1e643faf null
是的, 因此在Java11之後的關係應該是, AppClassLoader -> PlatFormClassLoader -> BootStapClassLoader
loadClass
參考:
深刻理解ClassLoader工做機制(jdk1.8)
下面仍是以 jdk1.8爲基準
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //a: if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //b: c = findClass(name); // this is the defining class loader; record the stats PerfCounter.getParentDelegationTime().addTime(t1 - t0); PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); PerfCounter.getFindClasses().increment(); } } //c: if (resolve) { resolveClass(c); } return c; } }
源碼很簡單, 查找class是否已經被加載, 若是沒有加載:
a: 對應註釋中的a處, 調用parent的classLoader, 若是代碼都遵循雙親委託模型, 則會一層層向上調用, 直到BootstrapClassLoader.
而parent並不是是繼承關係, 是經過組合的形式關聯起來.
private final ClassLoader parent;
只有當父類沒法加載當前類調用子類的findClass()方法去加載當前類, 層層向下.
b: 註釋b處
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
在ClassLoader類中findClass源碼如上, 不難發現, 依循雙親委託模型, 只須要實現本身的findClass方法便可. 這纔是咱們在本身的實現類中, 惟一要作的事情.
而當咱們拿到對應的Class文件, 或字節流形式的文件, 究竟該以怎樣的方式實現本身的findClass, 進而加載這個類呢?
I: Class<?> defineClass(String name, java.nio.ByteBuffer b,ProtectionDomain protectionDomain)
指定保護域(protectionDomain),把ByteBuffer的內容轉換成 Java 類。
II: Class<?> defineClass(String name, byte[] b, int off, int len)
把字節數組 b中的內容轉換成 Java 類
正是經過這個方法, 咱們能夠把本地文件或網絡流傳入, 進而生成相應的class, 加載進jvm虛擬機.
而另外一個方法也就是註釋c處:
protected final void resolveClass(Class<?> c) { //native方法 resolveClass0(c); }
當類被加載進來以後是否須要進行相關的連接操做也正是由這個參數所指定.
連接: 也就是 驗證, 準備, 解析相關操做.
至於具體的重寫 findCLass() , 案例中就有或者網上百度一大堆, 再也不多說.
雙親委派模型
提到過了, 雙親委派模型是層層向上, 直到最頂層, 若是頂層沒法加載類, 再層層向下, 直到拋出ClassNotFoundException; 其中父級與子類之間並不用繼承關係進行捆綁, 而是用組合的形式進行綁定.
那麼爲何要使用雙親委派模型?
若是咱們本身定義了一個 java.lang.String 放在classpath下, 那麼當使用時, 系統中就會出現多個 String類, String的種種行爲就沒法保證, 而若是有人上傳的框架或插件中, 偷偷的放入了本身的 java.lang.String, 並用來作一些壞事, 使用插件的人又該怎麼預防呢?
所以才須要從上到下一級級進行加載, 若是當前類已經被加載過了, 那麼就再也不會被加載. 這就保證了即便定義了本身的 java.lang.String 也會被BootstrapClassLoader加載到正確的 String類, 本身定義的永遠不會執行.
破壞雙親委託模型
前面提到的類加載器的代理模式並不能解決 Java 應用開發中會遇到的類加載器的所有問題。Java 提供了不少服務提供者接口(Service Provider Interface,SPI),容許第三方爲這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供.
基礎類之因此被稱爲"基礎",是由於它們老是做爲被調用代碼調用的API。
可是,若是基礎類又要調用用戶的代碼,那該怎麼辦呢。這並不是是不可能的事情,一個典型的例子即是JNDI服務, 它的代碼由啓動類加載器去加載(在JDK1.3時放進rt.jar)
但JNDI的目的就是對資源進行集中管理和查找,它須要調用獨立廠商實現部部署在應用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代碼,但啓動類加載器不可能"認識"之些代碼,該怎麼辦?
線程上下文類加載器正好解決了這個問題。若是不作任何的設置,Java 應用的線程的上下文類加載器默認就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就能夠成功的加載到 SPI 實現的類。線程上下文類加載器在不少 SPI 的實現中都會用到。
Thread.currentThread().getContextClassLoader();
至於想了解更多:
對我目前而言, 還未曾涉及到須要用到類加載相關的東西, 不進行相關框架設計, 談論更多也只是紙上談兵, 算不得數, 也與當前目的違背.