往期JVM系列:java
本節主要內容:數據庫
類的生命週期包含下面7個階段,其中前五步屬於類加載階段:數組
加載階段,虛擬機作了如下3件事情:安全
java.lang.Class
對象,做爲方法區這個類的各類數據的訪問入口簡單一句話歸納:把代碼數據加載到內存中,加載完成後,在方法區實例化一個對應的Class對象。bash
相對於類加載過程的其餘階段,一個非數組類的加載階段(準確地說,是加載階段中獲取類的二進制字節流的動做)是開發人員可控性最強的,由於加載階段既可使用系統提供的引導類加載器來完成,也能夠由用戶自定義的類加載器去完成,開發人員能夠經過定義本身的類加載器去控制字節流的獲取方式(即重寫一個類加載器的loadClass()
方法)。網絡
對於數組類而言,狀況就有所不一樣,數組類自己不經過類加載器建立,它是由Java虛擬機直接建立的。可是數組類的元素類型(Element Type,指的是數組去掉全部維度的類型)最終是要靠類加載器去建立,若是數組的組件類型不是引用類型(例如int[]數組),Java虛擬機將會把數組C標記爲與引導類加載器關聯。數據結構
數組類的可見性與它的組件類型的可見性一致,若是組件類型不是引用類型,那數組類的可見性將默認爲public
。多線程
當 JVM 加載完 Class 字節碼文件並在方法區建立對應的 Class 對象以後,JVM 便會啓動對該字節碼流的校驗,只有符合 JVM 字節碼規範的文件才能被 JVM 正確執行。這個校驗過程大體能夠四個階段:框架
文件格式驗證jvm
是否以魔法數0xCAFEBABE開頭、常量池的常量中是否有不被支持的常量類型等等。
該驗證階段的主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內,格式上符合描述一個Java類型信息的要求。這階段的驗證是基於二進制字節流進行的,只有經過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲。
只有經過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,因此後面的3個驗證階段所有是基於方法區的存儲結構進行的,不會再直接操做字節流。
元數據驗證
字節碼驗證
符號引用驗證
後面三個階段能夠概括爲代碼邏輯校驗,JVM 會對代碼組成的數據流和控制流進行校驗,確保 JVM 運行該字節碼文件後不會出現致命錯誤,好比final 是否合規、類型是否正確、靜態變量是否合理等。
簡單一句話歸納:驗證字節流信息符合當前虛擬機的要求,防止被篡改過的字節碼危害JVM安全。
當完成字節碼文件的校驗以後,JVM 便會開始爲類變量分配內存並設置類變量初始值。
類變量是被 static 修飾的變量,準備階段爲類變量分配內存並設置初始值,使用的是方法區的內存。
實例變量不會在這階段分配內存,它會在對象實例化時隨着對象一塊兒被分配在堆中。應該注意到,實例化不是類加載的一個過程,類加載發生在全部實例化操做以前,而且類加載只進行一次,實例化能夠進行屢次。
注意:這裏的初始化指的是爲變量賦予 Java 語言中該數據類型的零值,而不是用戶代碼裏初始化的值。
數據類型 | 零值 | 數據類型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte)0 |
簡單一句話歸納:爲靜態變量分配內存,而且設置默認值。
這裏舉一個「特殊」知識點。
上面提到,在「一般狀況」下初始值是零值,那相對的會有一些「特殊狀況」:若是類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化爲ConstantValue屬性所指定的值,假設上面類變量value的定義變爲:
public static final int value = 123;
複製代碼
在編譯時Javac將會爲被static和final修改的常量生成ConstantValue屬性
編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲123。
爲何 static final 會直接被賦值?final 關鍵字在 Java 中表明不可改變的意思,意思就是說 value 的值一旦賦值就不會在改變了。既然一旦賦值就不會再改變,那麼就必須一開始就給其賦予用戶想要的值,所以被 final 修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final 修飾的類變量,其可能在初始化階段或者運行階段發生變化,因此就沒有必要在準備階段對它賦予用戶想要的值。
當經過準備階段以後,JVM 針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類引用進行解析。這個階段的主要任務是將其在常量池中的符號引用替換成直接其在內存中的直接引用。
其中解析過程在某些狀況下能夠在初始化階段以後再開始,這是爲了支持 Java 的動態綁定。
簡單一句話歸納:解析類和方法,將常量池的符號引用替換爲直接引用,確保類與類之間相互引用正確性,完成內存結構佈局。
類初始化階段是類加載過程的最後一步,前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼(或者說是字節碼)。
簡單一句話歸納:初始化階段,執行類構造器
<clinit>()
方法(類變量賦值、靜態語句塊),若是賦值運算是經過其餘類的靜態方法來完成的,那麼會立刻解析另一個類,在虛擬機棧中執行完畢後經過返回值進行賦值。
<clinit>()
方法是由編譯器自動收集類中的全部類變量的賦值動做和**靜態語句塊(static{}塊)**中的語句合併產生的,編譯器收集的順序由語句在源文件中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它以前的類變量,定義在它以後的類變量只能賦值,不能訪問。例如如下代碼:
public class Test {
static {
i = 0; // 給變量賦值能夠正常編譯經過
System.out.print(i); // 這句編譯器會提示「非法向前引用」
}
static int i = 1;
}
複製代碼
因爲父類的 <clinit>()
方法先執行,也就意味着父類中定義的靜態語句塊的執行要優先於子類。例如如下代碼:
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
複製代碼
接口中不可使用靜態語句塊,但仍然有類變量初始化的賦值操做,所以接口與類同樣都會生成 <clinit>()
方法。但接口與類不一樣的是,執行接口的 <clinit>()
方法不須要先執行父接口的 <clinit>()
方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的 <clinit>()
方法。
虛擬機會保證一個類的<clinit>()
方法在多線程環境下被正確的加鎖和同步。
若是多個線程同時初始化一個類,只會有一個線程執行這個類的 <clinit>()
方法,其它線程都會阻塞等待,直到活動線程執行 <clinit>()
方法完畢。若是在一個類的 <clinit>()
方法中有耗時的操做,就可能形成多個線程阻塞,在實際過程當中此種阻塞很隱蔽。
示例代碼以下:
public class DeadLoopClassDemo {
static class DeadLoopClass {
static {
/*若是不加上這個if語句,編譯器將提示"Initializer does not complete normally"並拒絕編譯*/
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = () -> {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + "run over");
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
複製代碼
兩個類相等,須要類自己相等,而且使用同一個類加載器進行加載。這是由於每個類加載器都擁有一個獨立的類名稱空間。
這裏的相等,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果爲 true,也包括使用 instanceof 關鍵字作對象所屬關係斷定結果爲 true。
從 Java 虛擬機的角度來說,只存在如下兩種不一樣的類加載器:
從 Java 開發人員的角度看,類加載器能夠劃分得更細緻一些,相似於原始部落結構,存在權力等級制度。
C++
實現,是虛擬機自身的一部分;<JAVA_HOME>/lib
路徑下的核心類庫,沒法被Java程序直接引用。Object
, System
, String
等<JAVA_HOME>/lib/ext
路徑下的擴展類庫,開發者能夠直接使用擴展類加載器低層次的當前類加載器,不能覆蓋更高層次類加載器已經加載的類。若是低層次的類加載器想加載一個未知類,要很是禮貌地向上逐級詢問 :「請問,這個類已經加載了嗎?」 被詢問的高層次類加載器會自問兩個問題,第一,我是否已加載過此類?第二,若是沒有,是否能夠加載此類?只有當全部高層次類加載器在兩個問題上的答案均爲「否」時,纔可讓當前類加載器加載這個未知類。如上圖所示,左側綠色箭頭向上逐級詢問是否已加載此類,直至 Bootstrap ClassLoader ,而後向下逐級嘗試是否可以加載此類,若是都加載不了,則通知發起加載請求的當前類加載器 ,准予加載。
簡單一句話: 一個類加載器首先將類加載請求轉發到父類加載器,只有當父類加載器沒法完成時才嘗試本身加載。
該模型要求除了頂層的啓動類加載器外,其它的類加載器都要有本身的父類加載器。這裏的父子關係通常經過組合關係(Composition)來實現,而不是繼承關係(Inheritance)。
採用雙親委派模式的是好處是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係,經過這種層級關能夠避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次防止惡意覆蓋Java核心API。
例如 java.lang.Object 存放在 rt.jar 中,若是編寫另一個 java.lang.Object 並放到 ClassPath 中,程序能夠編譯經過。因爲雙親委派模型的存在,因此在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先級更高,這是由於 rt.jar 中的 Object 使用的是啓動類加載器,而 ClassPath 中的 Object 使用的是應用程序類加載器。rt.jar 中的 Object 優先級更高,那麼程序中全部的 Object 都是這個 Object。
如下是抽象類 java.lang.ClassLoader 的代碼片斷,其中的 loadClass() 方法運行過程以下:先檢查類是否已經加載過,若是沒有則讓父類加載器去加載。當父類加載器加載失敗時拋出 ClassNotFoundException,此時嘗試本身去加載。
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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) {
try {
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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
複製代碼
java.lang.ClassLoader
的 loadClass()
實現了雙親委派模型的邏輯,自定義類加載器通常不去重寫它,可是須要重寫 findClass()
方法。
如示例:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
// TODO 從自定義路徑中加載指定類
return null;
}
}
複製代碼
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
}
複製代碼
執行結果:
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/classes
複製代碼
使用-XX:+TraceClassLoading
參數,能夠在啓動時觀察加載了哪一個jar包中的哪一個類。此參數在解決類衝突時特別實用。由於不一樣JVM環境對於加載類的順序並不是是一致的。
部分示例:
[Opened C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.io.Serializable from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Comparable from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.String from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.reflect.Type from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Class from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Cloneable from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.ClassLoader from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.System from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Throwable from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
......
複製代碼
因爲加載的類數量衆多,調試時很難捕捉到指定類的加載過程,這時可使用條件斷點功能。拿HashMap
的加載過程爲例,在ClassLoader#loadClass()
處打個條件斷點,效果以下,
若是本文有幫助到你,但願能點個贊,這是對個人最大動力。