天天看點

深入了解java虛拟機(三)---類加載機制1、類加載機制概述2、類加載的時機3、類加載過程4、類加載器

這部分内容都是參考《深入了解java虛拟機—jvm進階特性與最佳實戰》-周志明  這本書基本都是講的底層實作的理論,國内很少有這方面的著作

想罵人了,前邊花了大量功夫寫的文章,儲存的時候卡了一下,等出來再登入發現之前寫的都沒存下來,唉,算了,把我參考過的内容拼接一下,先看着吧,稍微修改一下。靠自己回憶的話有太多内容容易漏掉,這篇内容基本是原書的複述。

1、類加載機制概述

虛拟機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型,這就是 虛拟機的類加載機制。 在java中,類型的加載、連接配接和初始化過程都是在程式 運作期間完成的,這種政策雖然會帶來一些性能開銷,但是卻為java應用程式提供了高度的靈活性,java 動态擴充的語言特性就是 依賴運作期動态加載和動态連結這個特點形成的,所謂java動态擴充,比如,如果編寫了一個面向接口的應用程式,可以等到運作時再指定其實際的實作類。

2、類加載的時機

類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,整個生命周期包括:加載、驗證、準備、解析、初始化、使用、解除安裝,共七個階段。其中,驗證、準備、解析3個階段稱為連接配接(Linking),7個過程發生順序如下:

深入了解java虛拟機(三)---類加載機制1、類加載機制概述2、類加載的時機3、類加載過程4、類加載器

上面這七個過程,除了解析這個過程外,其餘過程必須按部就班地執行,即順序是确定的,而解析過程不一定,在某些情況下可以在初始化階段之後再執行,這是為了支援java語言的運作時綁定(也稱為動态綁定或晚期綁定)。

java虛拟機規範中,并沒有規定類加載過程中的第一個階段(即加載階段)的執行時機,但是對于 初始化階段,虛拟機規範中嚴格規定了“ 有且隻有”下面5種情況下必須立即對類進行初始化(而這時,加載、驗證、準備自然需要在此之前開始): (1)遇到new、getstatic、putstatic、invokestatic這四條指令時,必須觸發其初始化。這四條指令最常見的場景是:使用new關鍵字執行個體化對象、讀取或設定一個類的靜态字段(被final修飾、已經在編譯期把結果放入常量池的靜态字段除外,即常量除外)、調用一個類的靜态方法的時候; (2)進行反射調用的時候; (3)初始化一個類的時候,如果其父類還沒有初始化,則需要先觸發其父類的初始化; (4)當虛拟機啟動時,需要先初始化那個包含main方法的要執行的主類; (5)當使用JDK1.7的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果為 REF_getStatic 、REF_putStatic、REF_invokeStatic的方法句柄,句柄對應的類會被初始化;

上面五個場景通俗的說法就是

(1) 建立類的執行個體,也就是new的方式

(2) 通路某個類或接口的靜态變量,或者對該靜态變量指派

(3) 調用類的靜态方法

(4) 反射(如Class.forName(“com.shengsiyuan.Test”))

(5) 初始化某個類的子類,則其父類也會被初始化

(6) Java虛拟機啟動時被标明為啟動類的類(Java Test),直接使用java.exe指令來運作某個主類
           

第五種我也沒搞太明白,如果有明白的,歡迎留言告訴我,哈哈。。。

上面五種場景觸發類 進行初始化的行為稱為對一個類進行“主動引用”,除此之外,所有其他引用類的方式 都不會觸發初始化步驟(注意,此時已經是引用了,隻不過不會觸發初始化,其他階段是否觸發要看具體虛拟機的實作),這些引用稱為“被動引用”。 被動引用的幾個例子: (1)對于靜态字段,隻有 直接定義這個字段的類才會被初始化,是以通過其子類來 引用父類中定義的靜态字段,隻會觸發 父類的初始化而不會觸發子類的初始化。至于是否要出發子類的加載、驗證需要看具體虛拟機實作;如下: [java]  view plain  copy

  1. class SuperClass{  
  2.     static{  
  3.         System.out.println("SuperClass init!");  
  4.     }  
  5.     public static int value = 123;  
  6. }  
  7. class SubClass extends SuperClass{  
  8.     static{  
  9.         System.out.println("SubClass init!");//子類中引用父類的靜态字段,不會導緻類初始化  
  10.     }  
  11. }  
  12. public class Test {  
  13.     public static void main(String[] args) {  
  14.         System.out.println(SubClass.value);  
  15.     }  
  16. }  

運作結果: SuperClass init! 123 可以看到,隻會列印出父類的初始化語句。

(2)通過數組定義來引用類,不會觸發此類的初始化。 如 A[] ints = new A[10] ,  不會觸發A 類的初始化。而是會觸發名為 LA的類初始化。它是一個由虛拟機自動生成的、直接繼承于Object 的子類,建立動作由位元組碼指令 newarray 觸發。這個類代表了一個元素類型為 A 的一位數組,數組中的屬性和方法都實作在這個類中。Java 語言中數組的通路比C/C++ 安全是因為這個類封裝了數組元素的通路方法。如下: public class Test {     public static void main(String[] args) {         SuperClass[] sca = new SuperClass[10];     } } SuperClass類為上面的那個,運作後發現并沒有列印出SuperClass init!,說明沒有觸發SuperClass類的初始化階段。

(3)常量在 編譯階段會存入 調用類的常量池中,本質上并沒有直接引用到定義常量的類,是以不會觸發定義常量的類的初始化,如下: [java]  view plain  copy

  1. class ConstClass{  
  2.     static{  
  3.         System.out.println("ConstClass init!");  
  4.     }  
  5.     public static final String HELLOWORLD = "hello world";  
  6. }  
  7. public class Test {  
  8.     public static void main(String[] args) {  
  9.         System.out.println(ConstClass.HELLOWORLD);  
  10.     }  
  11. }  

運作結果: hello world 隻是輸出了 hello world,并沒有輸出ConstClass init!,可見ConstClass類并沒有被初始化。

注意: 上面講的三個例子是被動引用的情況,很多情況下我們會通過new來初始化一個類,這個情形它屬于上面提到的5種主動引用的場景,是以會觸發這個類的初始化,如果這個類有父類的話,會先觸發父類的初始化。注意不要和上面的被動引用搞混了。

接口的初始化 上面代碼中用static語句塊進行初始化,而結構中不能使用static語句塊,但是編譯器仍然回味接口生成<clinit>()類構造器來初始化接口中的成員變量(常量);接口與類初始化的差別主要是在上面五種主動引用中的第三種:當一個類在初始化時,要求其父類全部已經初始化過了,但是對于 接口的初始化來說,并 不要求其 父接口全部都完成了初始化,隻有在 真正使用到付接口的時候(如引用接口中定義的常量)才會初始化。

3、類加載過程

3.1 加載

在加載階段,需要完成三件事情:

(1)通過一個類的全限定名來擷取其定義的二進制位元組流。

(2)将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。

(3)在記憶體中生成一個代表這個類的java.lang.Class對象(并沒有明确規定是在java堆中,對于HotSpot虛拟機來說,Class對象比較特殊,它雖然是對象,但是存放在方法區裡面),作為對方法區中這些資料的通路入口。

對于(1),并沒有指明二進制位元組流的擷取途徑,也即不一定都是從一個Class檔案中擷取,還可以從如下方式擷取:

    1)從壓縮包中擷取,比如 JAR包、EAR、WAR包等     2)從網絡中擷取,比如紅極一時的Applet技術     3)從運作過程中動态生成,最出名的便是動态代理技術,在java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 來為特定接口生成形式為“$Proxy”的代理類的二進制流     4)從其它檔案生成,如JSP檔案生成Class 類     5)從資料庫中讀取,比如說有些中間件伺服器,通過資料庫完成程式代碼在叢集之間的分發

相對于類加載過程的其他階段,加載這一步驟是開發人員可控的,即可以通過自定義類加載器來控制加載過程。

對于數組來說,數組類 本身不通過類加載器建立,它是由Java虛拟機直接建立的,但是 數組的元素類型,最終是要靠類加載器去建立。

3.2 驗證

驗證階段的目的是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。 Java語言本身是相對安全的,因為使用純粹的java代碼無法做到諸如通路數組邊界意外的資料、講一個對象轉型為它并未實作的類型、跳轉到不存在的代碼行之類的事情,如果我們這樣做了,那編譯器将拒絕編譯,也就保證了安全。但是前面說過,Class檔案并不一定要用Java源碼編譯而來,它還可以從很多途徑産生,在位元組碼層面,其他方式可能能做到java代碼無法做到的事情,是以虛拟機需要對加載盡量的位元組流進行驗證。驗證過程分為四步: (1)檔案格式驗證 這一階段是要驗證位元組流是否符合Class檔案格式的規範,并且能被目前版本的虛拟機處理。包括以下這些驗證點:     - 是否以魔數0xCAFEBABE開頭     - 主、次版本号是否在目前虛拟機處理範圍之内     - 常量池的常量中是否有不被支援的常量類型(檢查常量tag标志)     - 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量     - CONSTANT_Utf8_info 型的常量中是否有不符合UTF8 編碼的資料     - Class 檔案中各個部分以及檔案本身是否有被删除的或被附加的其它資訊     ... 這一階段驗證的 目的是保證輸入的位元組流能正确的解析并存儲到方法區中,這階段是 基于二進制位元組流進行的,通過驗證後,位元組流才會進入到記憶體的方法區中進行存儲。是以,後面的3個驗證階段是基于方法區的存儲結構進行分析的,不會再直接操作位元組流了。

(2)中繼資料驗證 對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求,主要是驗證類的繼承關系、資料類型是否符合,驗證點包括:     - 這個類是否有父類(除Object類外,其他所有類都應當有父類)     - 這個類的父類是否繼承了不允許被繼承的類(final 修飾的類)     - 這個類如果不是抽象類,是否實作了其父類或接口之中要求實作的所有方法     - 類中的字段、方法是否和父類産生沖突(如覆寫了父類final 字段,出現了非法的方法重載,如方法參數一緻,但傳回類型卻不同)

(3)位元組碼驗證 最複雜的一個階段,主要目的是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。在中繼資料驗證階段對資料類型做完校驗後,這個階段将對 類的方法體進行校驗分析,以保證被 校驗類的方法在運作時不會做出危害虛拟機安全的事件,有如下一些驗證點:     - 保證任何時候,操作數棧的資料類型與指令代碼序列都能配合工作,例如不會出現類似這樣的情況:在操作棧放入了一個int類型資料,使用時卻按 long 類型加載到本地變量表中     - 保證跳轉指令不會跳轉到方法體外的位元組碼指令上     - 保證方法體中類型轉換是有效的

(4)符号引用驗證 這一階段發生在虛拟機将符号引用轉化為直接引用的時候,而這個轉化動作發生在解析階段,符号引用可以看做是對 類自身以外(常量池中的各種符号引用)的資訊進行比對性校驗,驗證點如下:     - 符号引用中通過字元串描述的全限定名是否能找到相應的類     - 在指定類中對否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段     - 符号引用中的類、字段、方法的通路性(private、protected、public、default)是否可被目前類通路 這一階段驗證的目的是確定解析動作能正常執行。

對于虛拟機來說,驗證階段是一個非常重要的,但不是一定必要(因為對程式運作期沒有影響)的的階段。

3.3 準備

準備階段是正式為 類變量配置設定記憶體并設定 類變量初始值的階段,這些變量所使用的記憶體都将在 方法區中進行配置設定。有兩點需要注意: (1)這階段進行記憶體配置設定的 僅包括類變量(即被static修飾的變量),不包括執行個體變量, 執行個體變量會在對象執行個體化時随着對象一起配置設定在Java堆中; (2)這裡所說的初始值“通常情況”下是資料類型的零值,假設一個類變量的定義如下:     public static int value = 123; 那變量value在準備階段過後的零值為0而不是123,因為這時候并未執行任何Java方法,把 value指派為123的動作是在初始化階段才會進行。對于“非通常情況”,是指定義為常量的那些變量(即final修飾的),會在這一階段就被指派,如:     public static final int value = 123; 此時在準備階段過後,value的值将會被指派為123。

3.4 解析

解析階段是虛拟機将常量池中的符号引用轉化為直接引用的過程。

    - 符号引用(Symbolic References):即用一組符号來描述所引用的目标。它與虛拟機的 記憶體布局無關,引用的目标不一定已經加載到記憶體中。     - 直接引用(Direct References):直接引用可以是指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。它是和虛拟機記憶體布局相關的,如果有了直接引用,那引用的目标必定已經在記憶體中存在了。 解析動作主要針對 類或接口、字段、類方法、接口方法、方法類型、方法句柄 和 調用限定符 7類符号引用進行。

(1)類或接口的解析 判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,進而進行不同的解析。

(2)字段解析 在對字段進行解析前,會先檢視該字段所屬的類或接口的符号引用是否已經解析過,沒有就先對字段所屬的接口或類進行解析。在對字段進行解析的時候,先查找本類或接口中是否有該字段,有就直接傳回;否則,再對實作的接口進行周遊, 會按照繼承關系從下往上遞歸(也就是說,每個父接口都會走一遍)搜尋各個接口和它的父接口,傳回最近一個接口的直接引用;再對繼承的父類進行周遊,會按照繼承關系從下往上遞歸(也就是說,每個父類都會走一遍)搜尋各個父類,傳回最近一個父類的直接引用。 (3)類方法解析 和字段解析搜尋步驟差不多,隻不過是先搜尋父類,再搜尋接口。 (4)接口方法解析 和類方法解析差不多,隻不過接口中不會有父類,是以隻需要對父接口進行搜尋即可。

3.5 初始化

初始化是類加載過程的最後一步,此階段才開始真正執行類中定義的Java程式代碼(或者說位元組碼,也僅限與執行<clinit>()方法)。在準備階段,我們已經給變量付過一次系統要求的初始值(零值),而在初始化階段,則會根據程式員的意願給類變量和其他資源指派。主要是通過<clinit>()方法來執行的:  (1)<clinit>()方法是由編譯器自動收集類中的所有 類變量的指派動作和 靜态語句塊中的語句合并産生的,編譯器收集的順序是由語句在源檔案中出現的順序所決定的, 靜态語句塊中隻能通路到定義在靜态語句塊之前的變量, 定義在它之後的變量,在 前面的靜态語句中可以指派,但是不能通路。 如下: [java]  view plain  copy

  1. public class Test {  
  2.     static{  
  3.         i = 0;//可以給變量指派,編譯通過  
  4.         System.out.println(i);//編譯不通過!!不能進行通路後面的靜态變量  
  5.     }  
  6.     static int i =1;  
  7. }  

有點與我們平常的認知相反,這裡是可以下指派,卻不能通路...

 (2)<clinit>()方法與執行個體構造器<init>()方法(類的構造函數)不同,它 不需要顯式地調用父類構造器,虛拟機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。是以,在虛拟機中 第一個被執行的<clinit>()方法的類肯定是java.lang.Object。

 (3)<clinit>()方法對于類或接口來說并不是必須的,如果一個類中沒有靜态語句塊,也沒有對類變量的指派操作,那麼編譯器可以不為這個類生成<clinit>()方法。

 (4)接口中不能使用靜态語句塊,但仍然有類變量(final static)初始化的指派操作,是以接口與類一樣會生成<clinit>()方法。但是接口與類不同的是:執行接口的<clinit>()方法 不需要先執行父接口的<clinit>()方法,隻有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實作類在初始化時也一樣不會執行接口的<clinit>()方法。

 (5)虛拟機會保證一個類的<clinit>()方法在多線程環境中被正确地加鎖和同步,如果多個線程同時去初始化一個類,那麼隻會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,那就可能造成多個線程阻塞,在實際應用中這種阻塞往往是很隐蔽的。

4、類加載器

前面說過,在類加載過程的第一個階段:加載階段,除了可以使用系統提供的引導類加載器外,還可以使用使用者自定義的類加載器,以便讓使用者決定如何去擷取所需要的類(是從Class檔案中?還是從jar、或者其他方式...可以自由決定)。

4.1 類和類加載器

任意一個類,都需要由加載它的類加載器和這個類本身共同确定其在Java 虛拟機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以表達的更通俗一些:比較兩個類是否相等,隻有在這兩個類是同一個類加載器加載的前提下才意義。否則,即使這兩個類來自同一個Class檔案,被同一個虛拟機加載,但隻要加載他們的類加載器不同,那這兩個類就必定不相等。

這裡的“相等”,包括代表類的 Class 對象的equals() 方法、isAssignableFrom() 方法、isInstance() 方法的傳回結果,也包括 instanceof 關鍵字對對象所屬關系判定等情況。下面代碼示範了不同類加載器對 instanceof 關鍵字運算的結果的影響。

[java]  view plain  copy

  1. public class ClassLoaderTest {    
  2.     public static void main(String[] args) throws Exception {    
  3.         ClassLoader myLoader = new ClassLoader() {    
  4.             @Override    
  5.             public Class<?> loadClass(String name)    
  6.                     throws ClassNotFoundException {    
  7.                 try {    
  8.                     String fileName = name.substring(name.lastIndexOf(".") + 1)    
  9.                             + ".class";    
  10.                     InputStream is = getClass().getResourceAsStream(fileName);    
  11.                     if (is == null) {    
  12.                         return super.loadClass(name);    
  13.                     }    
  14.                     byte[] b = new byte[is.available()];    
  15.                     is.read(b);    
  16.                     return defineClass(name, b, 0, b.length);    
  17.                 } catch (IOException e) {    
  18.                     throw new ClassNotFoundException(name);    
  19.                 }    
  20.             }    
  21.         };    
  22.         Class c = myLoader.loadClass("org.bupt.xiaoye.blog.ClassLoaderTest");    
  23.         Object obj = c.newInstance();    
  24.         System.out.println(obj.getClass());    
  25.         System.out.println(ClassLoaderTest.class);    
  26.         System.out.println(obj instanceof ClassLoaderTest);    
  27.     }    
  28. }  

運作結果如下: class org.bupt.xiaoye.blog.ClassLoaderTest   class org.bupt.xiaoye.blog.ClassLoaderTest   false 我們使用了一個自定義的類加載器去加載ClassLoaderTest,由第一句也可以看出這個對象也的确是ClassLoaderTest執行個體化出來的對象,但是這個對象在與類class org.bupt.xiaoye.blog.ClassLoaderTest 做屬性檢查的時候卻反悔了false,這就是因為虛拟機中 存在了兩個ClassLoaderTest類,一個由系統應用程式類加載器加載,一個由我們自定義的類加載器加載,雖然是 來自同一個Class檔案,但依然是兩個獨立的類。

是以, 類是否相等,取決于 類本身和 加載該類的類加載器是否是同一個類加載器。

4.2 雙親委派模型(這個是重點,一定要搞清楚)

從虛拟機的角度來講,隻存在兩種不同的類加載器:

    一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器用 C++  語言實作, 是虛拟機自身的一部分:

    另一種就是所有其它的類加載器, 這些類加載器用Java 語言實作,獨立于虛拟機外部,并且全都繼承與抽象類 java.lang.ClassLoader。

從Java 開發人員的角度來看,類加載器還可以劃分的更細緻一些,絕大多數Java 程式都會用到以下3種系統提供的類加載器:

   (1) 啟動類加載器 (Bootstrap ClassLoader) : 這個類加載器負責将存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數指定的路徑中的,并且是虛拟機識别的(僅按照檔案名識别,如rt.jar ,名字不符合類庫不會加載) 類庫加載到虛拟機記憶體中。啟動類加載器無法被 java 程式直接引用,如需要,直接使用 null 代替即可。    (2) 擴充類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader 實作,它負責加載<JAVA_HOME>\lib\ ext 目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴充類加載器。    (3) 應用程式類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher$AppClassLoader 實作。這個這個類加載器是 ClassLoader 中的getSystemClassLoader() 方法的傳回值,是以一般稱它為系統類加載器。它負責加載用 戶路徑(ClassPath)上所指定的類庫,開發者可以使用這個類加載器,如果 應用程式沒有自定義過自己的類加載器,一般情況下這個就是程式中預設的類加載器。

我們的應用程式都是由這3中類加載器互相配合進行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關系一般如下圖所示:

深入了解java虛拟機(三)---類加載機制1、類加載機制概述2、類加載的時機3、類加載過程4、類加載器

圖中的類加載器之間的這種層次關系,稱為類加載器的 雙親委派模型 。雙親委派模型要求除了頂層的啟動類加載器,其餘的類加載器都應該有自己的父類加載器。這裡類加載器之間的父子關系一般不會以繼承關系來實作,而是使用 組合關系 來複用父加載器的代碼。

雙親委派模型的工作過程是: 如果一個類加載器收到了類加載器的請求,它首先不會自己嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,是以所有的加載請求最終都應該傳送到頂層的啟動類加載器中,隻有當父類加載器回報自己無法完成這個加載請求(它的搜尋範圍中沒有找到所需的類時),子加載類才會嘗試自己去加載。

使用雙親委派模型的好處: 就是Java類随着它的類加載器一起具備了一種帶有優先級的層次關系。比如對于類Object來說,它存放在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器去加載,是以Object類在程式中的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類自己去加載的話,按照我們前面說的,如果使用者自己編寫了一個Object類,并放在程式的ClassPath中,那系統中将會出現多個不同的Object類,此時Java類型提醒中最基礎的行為也就無法保證了,應用程式也将變得混亂。

是以,雙親委派模型對于保證Java程式的穩定運作很重要,但是他的實作其實很簡單,實作雙親委派模型的代碼幾種在java.lang.ClassLoader的loadClass()方法之中,邏輯清晰易懂: 先檢查類是否被加載過,若沒有則調用父加載器的loadClass() 方法,若父加載器為空則預設使用啟動類加載器作為父加載器。如果父加載器失敗,抛出 ClassNotFoundException 異常後,再調用自己的 finClass() 方法進行加載 ,如下: [java]  view plain  copy

  1. protected Class<?> loadClass(String name, boolean resolve)    
  2.         throws ClassNotFoundException {    
  3.     synchronized (getClassLoadingLock(name)) {    
  4.         // 首先檢查類是否已經被加載過    
  5.         Class c = findLoadedClass(name);    
  6.         if (c == null) {    
  7.             long t0 = System.nanoTime();    
  8.             try {    
  9.                 if (parent != null) {    
  10.                     // 調用父類加載器加載    
  11.                     c = parent.loadClass(name, false);    
  12.                 } else {    
  13.                     c = findBootstrapClassOrNull(name);    
  14.                 }    
  15.             } catch (ClassNotFoundException e) {    
  16.                 // ClassNotFoundException thrown if class not found    
  17.                 // from the non-null parent class loader    
  18.             }    
  19.             if (c == null) {    
  20.                 // If still not found, then invoke findClass in order    
  21.                 // to find the class.    
  22.                 //父類加載器無法完成加載,調用本身的加載器加載  
  23.                 long t1 = System.nanoTime();    
  24.                 c = findClass(name);    
  25.                 // this is the defining class loader; record the stats    
  26.                 sun.misc.PerfCounter.getParentDelegationTime().addTime(    
  27.                         t1 - t0);    
  28.                 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(    
  29.                         t1);    
  30.                 sun.misc.PerfCounter.getFindClasses().increment();    
  31.             }    
  32.         }    
  33.         if (resolve) {    
  34.             resolveClass(c);    
  35.         }    
  36.         return c;    
  37.     }    
  38. }  

好了,就總結這麼多了,過一段時間可以回頭看一下

(注:

内容來源:https://blog.csdn.net/shakespeare001/article/details/51765353

文中圖檔來源于: http://blog.csdn.net/zq602316498/article/details/38871785

http://blog.csdn.net/zq602316498/article/details/38902355