首先Throws(拋出)幾個自己學習過程中一直疑惑的問題:
1、什么是類加載?什么時候進行類加載?
2、什么是類初始化?什么時候進行類初始化?
3、什么時候會為變量分配內存?
4、什么時候會為變量賦默認初值?什么時候會為變量賦程序設定的初值?
5、類加載器是什么?
6、如何編寫一個自定義的類加載器?
首先,在代碼編譯后,就會生成JVM(Java虛擬機)能夠識別的二進制字節流文件(*.class)。而JVM把Class文件中的類描述數據從文件加載到內存,並對數據進行校驗、轉換解析、初始化,使這些數據最終成為可以被JVM直接使用的Java類型,這個說來簡單但實際復雜的過程叫做JVM的類加載機制。
Class文件中的“類”從加載到JVM內存中,到卸載出內存過程有七個生命周期階段。類加載機制包括了前五個階段。
如下圖所示:

其中,加載、驗證、准備、初始化、卸載的開始順序是確定的,注意,只是按順序開始,進行與結束的順序並不一定。解析階段可能在初始化之后開始。
另外,類加載無需等到程序中“首次使用”的時候才開始,JVM預先加載某些類也是被允許的。(類加載的時機)
一、類的加載
我們平常說的加載大多不是指的類加載機制,只是類加載機制中的第一步加載。在這個階段,JVM主要完成三件事:
1、通過一個類的全限定名(包名與類名)來獲取定義此類的二進制字節流(Class文件)。而獲取的方式,可以通過jar包、war包、網絡中獲取、JSP文件生成等方式。
2、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。這里只是轉化了數據結構,並未合並數據。(方法區就是用來存放已被加載的類信息,常量,靜態變量,編譯后的代碼的運行時內存區域)
3、在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。這個Class對象並沒有規定是在Java堆內存中,它比較特殊,雖為對象,但存放在方法區中。
二、類的連接
類的加載過程后生成了類的java.lang.Class對象,接着會進入連接階段,連接階段負責將類的二進制數據合並入JRE(Java運行時環境)中。類的連接大致分三個階段。
1、驗證:驗證被加載后的類是否有正確的結構,類數據是否會符合虛擬機的要求,確保不會危害虛擬機安全。
2、准備:為類的靜態變量(static filed)在方法區分配內存,並賦默認初值(0值或null值)。如static int a = 100;
靜態變量a就會在准備階段被賦默認值0。
對於一般的成員變量是在類實例化時候,隨對象一起分配在堆內存中。
另外,靜態常量(static final filed)會在准備階段賦程序設定的初值,如static final int a = 666; 靜態常量a就會在准備階段被直接賦值為666,對於靜態變量,這個操作是在初始化階段進行的。
3、解析:將類的二進制數據中的符號引用換為直接引用。
三、類的初始化
類初始化是類加載的最后一步,除了加載階段,用戶可以通過自定義的類加載器參與,其他階段都完全由虛擬機主導和控制。到了初始化階段才真正執行Java代碼。
類的初始化的主要工作是為靜態變量賦程序設定的初值。
如static int a = 100;在准備階段,a被賦默認值0,在初始化階段就會被賦值為100。
Java虛擬機規范中嚴格規定了有且只有五種情況必須對類進行初始化:
1、使用new字節碼指令創建類的實例,或者使用getstatic、putstatic讀取或設置一個靜態字段的值(放入常量池中的常量除外),或者調用一個靜態方法的時候,對應類必須進行過初始化。
2、通過java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則要首先進行初始化。
3、當初始化一個類的時候,如果發現其父類沒有進行過初始化,則首先觸發父類初始化。
4、當虛擬機啟動時,用戶需要指定一個主類(包含main()方法的類),虛擬機會首先初始化這個類。
5、使用jdk1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行初始化,則需要先觸發其初始化。
注意,虛擬機規范使用了“有且只有”這個詞描述,這五種情況被稱為“主動引用”,除了這五種情況,所有其他的類引用方式都不會觸發類初始化,被稱為“被動引用”。
被動引用的例子一:
通過子類引用父類的靜態字段,對於父類屬於“主動引用”的第一種情況,對於子類,沒有符合“主動引用”的情況,故子類不會進行初始化。代碼如下:
- //父類
- public class SuperClass {
- //靜態變量value
- public static int value = 666;
- //靜態塊,父類初始化時會調用
- static{
- System.out.println("父類初始化!");
- }
- }
- //子類
- public class SubClass extends SuperClass{
- //靜態塊,子類初始化時會調用
- static{
- System.out.println("子類初始化!");
- }
- }
- //主類、測試類
- public class NotInit {
- public static void main(String[] args){
- System.out.println(SubClass.value);
- }
- }
輸出結果:

被動引用的例子之二:
通過數組來引用類,不會觸發類的初始化,因為是數組new,而類沒有被new,所以沒有觸發任何“主動引用”條款,屬於“被動引用”。代碼如下:
- //父類
- public class SuperClass {
- //靜態變量value
- public static int value = 666;
- //靜態塊,父類初始化時會調用
- static{
- System.out.println("父類初始化!");
- }
- }
- //主類、測試類
- public class NotInit {
- public static void main(String[] args){
- SuperClass[] test = new SuperClass[10];
- }
- }
沒有任何結果輸出!
被動引用的例子之三:
剛剛講解時也提到,靜態常量在編譯階段就會被存入調用類的常量池中,不會引用到定義常量的類,這是一個特例,需要特別記憶,不會觸發類的初始化!
- //常量類
- public class ConstClass {
- static{
- System.out.println("常量類初始化!");
- }
- public static final String HELLOWORLD = "hello world!";
- }
- //主類、測試類
- public class NotInit {
- public static void main(String[] args){
- System.out.println(ConstClass.HELLOWORLD);
- }
- }

