咱們寫的Java代碼,通過編譯器編譯以後,就成爲了.class
文件,從本地機器碼變成了字節碼。Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。Class文件中只有2種數據結構:無符號數和表。html
每一個Class文件的頭4個字節稱爲魔數(Magic Number),值爲0xCAFEBABE
。緊接着4個字節是Class文件的版本號。再日後,就是類的具體信息了,好比常量池、類索引、父類索引、接口索引、字段、方法等信息了。java
所謂類的加載,就是把Class文件讀到內存中。mysql
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。注意,是循序漸進地「開始」,而不是循序漸進地「進行」或「完成」,強調這點是由於這些階段一般都是互相交叉地混合式進行的,一般會在一個階段執行的過程當中調用、激活另一個階段。sql
在加載階段,虛擬機作3件事:緩存
java.lang.Class
對象,做爲方法區這個類的各類數據的訪問入口。驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。安全
驗證階段大體上會完成4個階段的檢驗動做數據結構
0xCAFEBABE
開頭,主、次版本號是否能被當前虛擬機處理,常量類型,指向常量的索引是否符合要求等。這階段的驗證是基於二進制字節流進行的,只有經過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,因此後面的3個驗證階段所有是基於方法區的存儲結構進行的,不會再直接操做字節流。驗證階段是很是重要的,但不是必須的。它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverify:none
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。多線程
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念須要強調一下,首先,這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值。jvm
假設一個類變量的定義爲:public static int value = 123;源碼分析
那變量value在準備階段事後的初始值爲0而不是123,由於這時候還沒有開始執行任何Java 方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器<clinit>()方 法之中,因此把value賦值爲123的動做將在初始化階段纔會執行。
固然也有特殊狀況:若是類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化爲ConstantValue屬性所指定的值。
假設上面類變量value的定義變爲:public static final int value = 123;
編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲123。
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
這一步開始執行類中定義的Java程序代碼(或者說是字節碼)。虛擬機會保證一個類的初始化方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的初始化方法,其餘線程都須要阻塞等待,直到活動線程執行完畢。
只有當主動使用一個類的時候纔會觸發這個類的初始化,類的主動使用包括如下六種:
Class.forName("com.mysql.jdbc.Driver")
虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到Java虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的代碼模塊稱爲「類加載器」。
從Java虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另外一種就是全部其餘的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader。
從Java開發人員的角度來看,類加載器能夠劃分爲如下3種:
-Xbootclasspath
參數指定的路徑中的,而且能被虛擬機識別的類庫(如rt.jar,全部的java.開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器是沒法被Java程序直接引用的。sun.misc.Launcher$ExtClassLoader
實現,它負責加載JAVA_HOME\lib\ext目錄中的,或者被java.ext.dirs
系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。sun.misc.Launcher$AppClassLoader
來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。咱們的應用程序都是由這3種類加載器互相配合進行加載的,若是有必要,還能夠加入 本身定義的類加載器。
雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。這裏類加載器之間的父子關係通常不會以繼承
的關係來實現,而是都使用組合
關係來複用父加載器的代碼。它不是強制性的約束模型,而是Java設計者推薦的一種類加載器實現方式。
雙親委派模型的工做過程:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
ClassLoader源碼分析:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先判斷該類型是否已經被加載
Class c = findLoadedClass(name);
if (c == null) {
//若是沒有被加載,就委託給父類加載或者委派給啓動類加載器加載
try {
if (parent != null) {
//若是存在父類加載器,就委派給父類加載器加載
c = parent.loadClass(name, false);
} else {
//若是不存在父類加載器,就檢查是不是由啓動類加載器加載的類,經過調用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 若是父類加載器和啓動類加載器都不能完成加載任務,才調用自身的加載功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
複製代碼
經過分析源碼,咱們知道,雙親委派模型能夠保證每一個類都只會被加載一次(相似緩存機制)。