本文是第十章的一些筆記整理。java
本文主要講述了類加載器以及類加載的詳細流程。數組
類加載的流程能夠簡單分爲三步:安全
而其中的鏈接又能夠細分爲三步:性能優化
下面會分別對各個流程進行介紹。bash
在瞭解類接在流程以前,先來看一下觸發類加載的條件。網絡
JVM
不會無條件加載類,只有在一個類或接口在初次使用的時候,必須進行初始化。這裏的使用是指主動使用,主動使用包括以下狀況:數據結構
new
建立,或者使用反射、克隆、反序列化invokestatic
指令getstatic
/putstatic
指令java.lang.reflect
中的反射類方法時main()
方法的類除了以上狀況外,其餘狀況屬於被動使用,不會引發類的初始化。多線程
好比下面的例子:ide
public class Main { public static void main(String[] args){ System.out.println(Child.v); } } class Parent{ static{ System.out.println("Parent init"); } public static int v = 100; } class Child extends Parent{ static { System.out.println("Child init"); } }
輸出以下:函數
Parent init 100
而加上類加載參數-XX:+TraceClassLoading
後,能夠看到Child
確實被加載了:
[0.068s][info ][class,load] com.company.Main [0.069s][info ][class,load] com.company.Parent [0.069s][info ][class,load] com.company.Child Parent init 100
可是並無進行初始化。另一個例子是關於final
的,代碼以下:
public class Main { public static void main(String[] args){ System.out.println(Test.STR); } } class Test{ static{ System.out.println("Test init"); } public static final String STR = "Hello"; }
輸出以下:
[0.066s][info ][class,load] com.company.Main Hello
Test
類根本沒有被加載,由於final
被作了優化,編譯後的Main.class
中,並無引用Test
類:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String Hello 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
在字節碼偏移3的位置,經過ldc
將常量池第4項入棧,此時在字節碼文件中常量池第4項爲:
#3 = Class #24 // com/company/Test #4 = String #25 // Hello #5 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
所以並無對Test
類進行加載,只是直接引用常量池中的常量,所以輸出沒有Test
的加載日誌。
類加載的時候,JVM
必須完成如下操做:
java.lang.Class
類的實例,表示該類型第一步獲取二進制數據流,途徑有不少,包括:
JAR
/ZIP
壓縮包等等,獲取到二進制數據流後,JVM
進行處理並轉化爲一個java.lang.Class
實例。
驗證的操做是確保加載的字節碼是合法、合理而且規範的。步驟簡略以下:
JVM
支持範圍內等等final
的方法或類是否被重載了或者繼承了,是否存在不兼容方法等等NoClassDefFoundError
,沒法找到方法就拋出NoSuchMethodError
類經過驗證後,就會進入準備階段,在這個階段,JVM
爲會類分配相應的內存空間,並設置初始值,好比:
int
初始化爲0
long
初始化爲0L
double
初始化爲0f
null
若是存在常量字段,那麼這個階段也會爲常量賦值。
解析就是將類、接口、字段和方法的符號引用轉爲直接引用。符號引用就是一些字面量引用,和JVM
的內存數據結構和內存佈局無關,因爲在字節碼文件中,經過常量池進行了大量的符號引用,這個階段就是將這些引用轉爲直接引用,獲得類、字段、方法在內存中的指針或直接偏移量。
另外,因爲字符串有着很重要的做用,JVM
對String
進行了特別的處理,直接使用字符串常量時,就會在類中出現CONSTANT_String
,而且會引用一個CONSTANT_UTF8
常量項。JVM
運行時,內部的常量池中會維護一張字符串拘留表(intern
),會保存其中出現過的全部字符串常量,而且沒有重複項。使用String.intern()
能夠得到一個字符串在拘留表的引用,好比下面代碼:
public static void main(String[] args){ String a = 1 + String.valueOf(2) + 3; String b = "123"; System.out.println(a.equals(b)); System.out.println(a == b); System.out.println(a.intern() == b); }
輸出:
true false true
這裏b
就是常量自己,所以a.intern()
返回在拘留表的引用後就是b
自己,比較結果爲真。
初始化階段會執行類的初始化方法<clint>
,<clint>
是由編譯期生成的,由靜態成員的賦值語句以及static
語句共同產生。
另外,加載一個類的時候,JVM
老是會試圖加載該類的父類,所以父類的<clint>
方法老是在子類的<clint>
方法以前被調用。另外一方面,須要注意的是<clint>
會確保在多線程環境下的安全性,也就是多個線程同時初始化同一個類時,只有一個線程能夠進入<clint>
方法,換句話說,在多線程下可能會出現死鎖,好比下面代碼:
package com.company; import java.util.concurrent.TimeUnit; public class Main extends Thread{ private char flag; public Main(char flag){ this.flag = flag; } public static void main(String[] args){ Main a = new Main('A'); a.start(); Main b = new Main('B'); b.start(); } @Override public void run() { try{ Class.forName("com.company.Static"+flag); }catch (ClassNotFoundException e){ e.printStackTrace(); } } } class StaticA{ static { try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } try{ Class.forName("com.company.StaticB"); }catch (ClassNotFoundException e){ e.printStackTrace(); } System.out.println("StaticA init ok"); } } class StaticB{ static { try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } try{ Class.forName("com.company.StaticA"); }catch (ClassNotFoundException e){ e.printStackTrace(); } System.out.println("StaticB init ok"); } }
在加載StaticA
的時候嘗試加載StaticB
,可是因爲StaticB
已經被加載中,所以加載StaticA
的線程會阻塞在Class.forName("com.company.StaticB")
處,同理加載StaticB
的線程會阻塞在Class.forName("com.company.StaticA")
處,這樣就出現死鎖了。
ClassLoader
ClassLoader
簡介ClassLoader
是類加載的核心組件,全部的Class
都是由ClassLoader
加載的,ClassLoader
經過各類各樣的方式將Class
信息的二進制數據流讀入系統,而後交給JVM
進行鏈接、初始化等操做。所以ClassLoader
負責類的加載流程,沒法經過ClassLoader
改變類的鏈接和初始化行爲。
ClassLoader
是一個抽象類,提供了一些重要接口定義加載流程和加載方式,主要方法以下:
public Class<?> loadClass(String name) throws ClassNotFoundException
:給定一個類名,加載一個類,返回這個類的Class
實例,找不到拋出異常protected final Class<?> defineClass(byte[] b, int off, int len)
:根據給定字節流定義一個類,off
和len
表示在字節數組中的偏移和長度,這是一個protected
方法,在自定義子類中才能使用protected Class<?> findClass(String name) throws ClassNotFoundException
:查找一個類,會在loadClass
中被調用,用於自定義查找類的邏輯protected Class<?> findLoadedClass(String name)
:尋找一個已經加載的類在標準的Java
程序中,JVM
會建立3類加載器爲整個應用程序服務,分別是:
Bootstrap ClassLoader
Extension ClassLoader
App ClassLoader
另外,在程序中還能夠定義本身的類加載器,從整體看,層次結構以下:
通常來講各個加載器負責的範圍以下:
rt.jar
包中的類lib/ext/*.jar
下的類默認狀況下,類加載使用雙親委派加載的模式,具體來講,就是類在加載的時候,會判斷當前類是否已經被加載,若是已經被加載,那麼直接返回已加載的類,若是沒有,會先請求雙親加載,雙親也是按照同樣的流程先判斷是否已加載,若是沒有在此委託雙親加載,若是雙親加載失敗,則會本身加載。
在上圖中,應用類加載器的雙親爲擴展類加載器,擴展類加載器的雙親爲啓動類加載器,當系統須要加載一個類的時候,會先從底層類加載器開始進行判斷,當須要加載的時候會從頂層開始加載,依次向下嘗試直到加載成功。
在全部加載器中,啓動類加載器是最特別的,並非使用Java
語言實現,在Java
中沒有對象與之相對應,系統核心類就是由啓動類加載器進行加載的。換句話說,若是嘗試在程序中獲取啓動類加載器,獲得的值是null
:
System.out.println(String.class.getClassLoader() == null);
輸出結果爲真。