3 準備
完成兩件事情
- 為已在方法區中的類的靜态成員變量配置設定記憶體
-
為靜态成員變量設定初始值
初始值為0、false、null等
public static final int value = 123;
準備階段後 a 的值為 0,而不是 123,要在初始化之後才變為 123,但若被final修飾的常量如果有初始值,那麼在編譯階段就會将初始值存入constantValue屬性中,在準備階段就将constantValue的值賦給該字段(此處将value賦為123).
4 解析
把常量池中的符号引用轉換成直接引用的過程。包括:
-
符号引用
以一組無歧義的符号來描述所引用的目标,與虛拟機的實作無關。
-
直接引用
直接指向目标的指針、相對偏移量、或是能間接定位到目标的句柄,是和虛拟機實作相關的。
主要針對:類、接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符。
5 初始化
真正開始執行類中定義的Java程式代碼(或說是位元組碼),類的初始化就是為類的靜态變量賦初始值,初始化階段就是執行類構造器<clinit>的過程。
如果類還沒有加載和連接配接,就先加載和連接配接
如果類存在父類,且父類沒有初始化,就先初始化父類
如果類中存在初始化語句,就依次執行這些初始化語句
如果是接口
初始化一個類時,并不會先初始化它實作的接口
初始化一個接口時,并不會初始化它的父接口
隻有當程式首次使用接口裡面的變量或者是調用接口方法的時候,才會導緻接口初始化
調用Classloader類的loadClass方法來裝載一個類,并不會初始化這個類,不屬于對類的主動使用
clinit()方法由編譯器自動産生,收集類中static{}代碼塊中的類變量指派語句和類中靜态成員變量的指派語句。
在準備階段,類中靜态成員變量已經完成了預設初始化,而在初始化階段,clinit()方法對靜态成員變量進行顯示初始化。
類的初始化時機
Java程式對類的使用方式分為:
- 主動使用
- 被動使用
JVM必須在每個類或接口“首次主動使用”時才初始化它們,被動使用類不會導緻類的初始化。主動使用的場景:
建立類執行個體
通路某個類或接口的靜态變量
如果是 final 常量,而常量在編譯階段就會在常量池,沒有引用到定義該常量的類,是以不會觸發定義該常量類的初始化
調用類的靜态方法
反射某個類
初始化某個類的子類,而父類還沒有初始化
JVM啟動的時候運作的主類(等于第三條)
定義了 default 方法的接口,當接口實作類初始化時
初始化過程的注意點
- clinit()方法是IDE自動收集類中所有類變量的指派動作和靜态語句塊中的語句合并産生的,IDE收集的順序是由語句在源檔案中出現的順序所決定的.
- 靜态代碼塊隻能通路到出現在靜态代碼塊之前的變量,定義在它之後的變量,在前面的靜态語句塊可以指派,但是不能通路.
public class Test {
static {
i=0;
System.out.println(i); //編譯失敗:"非法向前引用"
}
static int i = 1;
}
執行個體構造器init()需要顯式調用父類構造器,而類的clinit()無需調用父類的類構造器,JVM會確定子類的clinit()方法執行前已執行完畢父類的clinit()方法。
是以在JVM中第一個被執行的clinit()方法的類肯定是java.lang.Object.
如果一個類/接口無static代碼塊,也無 static成員變量的指派操作,則編譯器不會為此類生成clinit()方法
接口也需要通過clinit()方法為接口中定義的static成員變量顯示初始化。
接口中不能使用靜态代碼塊,但仍然有變量初始化的指派操作,是以接口與類一樣都會生成clinit()方法.不同的是,執行接口的clinit()方法不需要先執行父接口的clinit()方法.隻有當父接口中的靜态成員變量被使用到時才會執行父接口的clinit()方法.
虛拟機會保證在多線程環境中一個類的clinit()方法别正确地加鎖,同步.當多條線程同時去初始化一個類時,隻會有一個線程去執行該類的clinit()方法,其它線程都被阻塞等待,直到活動線程執行clinit()方法完畢.
其他線程雖會被阻塞,隻要有一個clinit()方法執行完,其它線程喚醒後不會再進入clinit()方法。同一個類加載器下,一個類型隻會初始化一次。
6 類的解除安裝
當代表一個類的Class對象不再被引用,那麼Class對象的生命周期就結束了,對應的在方法區中的資料也會被解除安裝。
Jvm自帶的類加載器裝載的類,是不會解除安裝的,由使用者自定義的類加載器加載的類是可以解除安裝的。
參考
- 《碼到成功》
- 《深入了解Java虛拟機第三版》