Java虛擬機類加載機製
看到這個題目,很多人會覺得我寫我的java代碼,至於類,JVM愛怎麼加載就怎麼加載,博主有很長一段時間也是這麼認為的。隨著編程經驗的日積月累,越來越感覺到了解虛擬機相關要領的重要性。閑話不多說,老規矩,先來一段代碼吊吊胃口。
public class SSClass
{
static
{
System.out.println("SSClass");
}
}
public class SuperClass extends SSClass
{
static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass()
{
System.out.println("init SuperClass");
}
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init");
}
static int a;
public SubClass()
{
System.out.println("init SubClass");
}
}
public class NotInitialization
{
public static void main(String[] args)
{
System.out.println(SubClass.value);
}
}
運行結果:
SSClass
SuperClass init!
123
答案答對了嚒?
也許有人會疑問:為什麼沒有輸出SubClass init。ok~解釋一下:對於靜態字段,隻有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態字段,隻會觸發父類的初始化而不會觸發子類的初始化。 如果你想學習Java可以來這個群,首先是一二六,中間是五三四,最後是五一九,裏麵有大量的學習資料可以下載。
上麵就牽涉到了虛擬機類加載機製。如果有興趣,可以繼續看下去。
類加載過程
類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱為連接(Linking)。如圖所示。
加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支持Java語言的運行時綁定(也稱為動態綁定或晚期綁定)。以下陳述的內容都已HotSpot為基準。
加載
在加載階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機需要完成以下3件事情:
1.通過一個類的全限定名來獲取定義此類的二進製字節流(並沒有指明要從一個Class文件中獲取,可以從其他渠道,譬如:網絡、動態生成、數據庫等);
2.將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;
3.在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口;
加載階段和連接階段(Linking)的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但這些夾在加載階段之中進行的動作,仍然屬於連接階段的內容,這兩個階段的開始時間仍然保持著固定的先後順序。
驗證
驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
驗證階段大致會完成4個階段的檢驗動作:
1.文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。
2.元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
3.字節碼驗證:通過數據流和控製流分析,確定程序語義是合法的、符合邏輯的。
4.符號引用驗證:確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反複驗證,那麼可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
準備
準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在堆中。其次,這裏所說的初始值“通常情況”下是數據類型的零值,假設一個類變量的定義為:
public static int value=123;
那變量value在準備階段過後的初始值為0而不是123.因為這時候尚未開始執行任何java方法,而把value賦值為123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。
至於“特殊情況”是指:public static final int value=123,即當類字段的字段屬性是ConstantValue時,會在準備階段初始化為指定的值,所以標注為final之後,value的值在準備階段初始化為123而非0.
解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
初始化
類初始化階段是類加載過程的最後一步,到了初始化階段,才真正開始執行類中定義的java程序代碼。在準備極端,變量已經付過一次係統要求的初始值,而在初始化階段,則根據程序猿通過程序製定的主管計劃去初始化類變量和其他資源,或者說:初始化階段是執行類構造器()方法的過程.
()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊static{}中的語句合並產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊隻能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前麵的靜態語句塊可以賦值,但是不能訪問。如下:
public class Test
{
static
{
i=0;
System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
}
static int i=1;
}
()方法與實例構造器()方法不同,它不需要顯示地調用父類構造器,虛擬機會保證在子類()方法執行之前,父類的()方法方法已經執行完畢,回到本文開篇的舉例代碼中,結果會打印輸出:SSClass就是這個道理。
由於父類的()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變量賦值操作。
()方法對於類或者接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生產()方法。
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成()方法。但接口與類不同的是,執行接口的()方法不需要先執行父接口的()方法。隻有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的()方法。
虛擬機會保證一個類的()方法在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那麼隻會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執行()方法完畢。如果在一個類的()方法中有好事很長的操作,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的。
package jvm.classload;
public class DealLoopTest
{
static class DeadLoopClass
{
static
{
if(true)
{
System.out.println(Thread.currentThread()+"init DeadLoopClass");
while(true)
{
}
}
}
}
public static void main(String[] args)
{
Runnable script = new Runnable(){
public void run()
{
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();
}
}
運行結果:(即一條線程在死循環以模擬長時間操作,另一條線程在阻塞等待)
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass
需要注意的是,其他線程雖然會被阻塞,但如果執行()方法的那條線程退出()方法後,其他線程喚醒之後不會再次進入()方法。同一個類加載器下,一個類型隻會初始化一次。
將上麵代碼中的靜態塊替換如下:
static
{
System.out.println(Thread.currentThread() + "init DeadLoopClass");
try
{
TimeUnit.SECONDS.sleep(10);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
運行結果:
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass (之後sleep 10s)
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over
虛擬機規範嚴格規定了有且隻有5中情況(jdk1.7)必須對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):
1.遇到new,getstatic,putstatic,invokestatic這失調字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
2.使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
3.當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
4.當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
5.當使用jdk1.7動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。
開篇已經舉了一個範例:通過子類引用付了的靜態字段,不會導致子類初始化。
這裏再舉兩個例子。
1. 通過數組定義來引用類,不會觸發此類的初始化:(SuperClass類已在本文開篇定義)
public class NotInitialization
{
public static void main(String[] args)
{
SuperClass[] sca = new SuperClass[10];
}
}
運行結果:(無)
2. 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化:
public class ConstClass
{
static
{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization
{
public static void main(String[] args)
{
System.out.println(ConstClass.HELLOWORLD);
}
}
運行結果:hello world
最後更新:2017-04-14 14:30:36