JVM的類載入過程總體來說分為三個階段:
1、類的載入
類的載入過程通過一個類的全限定名獲取定義此類的二進位制位元組流,然後將這個位元組流所代表的靜態資料結構轉化為方法區的執行時資料結構,最後在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法去這個類的各種資料的訪問入口。
載入.class檔案的方式:
- 從本地系統中直接載入
- 從zip壓縮包中讀取,成為日後jar、war格式的基礎
- 執行時計算,比如動態代理
- 由其他檔案生成,如jsp
- 從加密檔案獲取,比如使用自定義類載入器載入加密檔案,來保證Class檔案不會被反編譯
2、連結階段
a)驗證
確保class檔案的位元組流中包含的資訊符合虛擬機器要求,保證被載入類的正確性,不會危害到虛擬機器自身的安全,主要包括檔案格式驗證、後設資料驗證、位元組碼驗證、符號引用驗證。
b)準備
為類變數分配記憶體並且設定類變數的預設初始值,即零值。這裡不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯式初始化,比如說在這裡final static int a = 10;就不會把a設為0值了,而會直接分配記憶體,放入方法區中,並設初始值10。這裡不會為例項變數分配初始化,類變數會分配在方法區中,例項變數會隨著物件一起分配到堆中。
c)解析
通常解析操作往往伴隨著JVM在初始化後再執行,將符號引用轉換成直接引用,
3、初始化過程
初始化階段就是執行類構造器方法<clint>()的過程。
此方法不需要定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來。
如果該類具有父類,必須保證先執行父類的<clint>()再執行子類的<clint>(),並且一個類的<clint>()在多執行緒下被枷鎖。比如下方例子,a執行緒搶到執行權後,在初始化Test類的時候一直進入死迴圈,導致類始終無法初始化完畢,所以此時a執行緒無法初始完類,b執行緒也一直等待。
public class Main { public static void main(String[] args) { Runnable runnable = () -> { System.out.println(Thread.currentThread().getName() + "初始化開始"); Test t = new Test(); System.out.println(Thread.currentThread().getName() + "初始化結束"); }; Thread a = new Thread(runnable); Thread b = new Thread(runnable); a.start(); b.start(); } } class Test { static { if (true) { System.out.println("初始化當前類"); while (true) { } } } }
輸出:
Thread-0初始化開始
初始化當前類
Thread-1初始化開始