類載入的時機
類載入後要初始化。所以可以通過判斷啥時候要初始化,得出類載入的時機。
有且只有5種情況需要對類進行初始化
new
例項化物件、讀取或設定靜態欄位(被final修飾放入常量池時除外)、呼叫類的靜態方法 (且類沒有初始化)。- 使用
java.lang.reflect
包的方法對類進行反射呼叫,且類沒有初始化 - 初始化一個類時,如果父類沒有初始化,會觸發父類的初始化
- 含main方法的類會優先初始化
- 當使用JDK1.7的動態語言支援時,如果一個
java.lang.invoke.MethodHandle
例項最後的解析結果REF_getStatic、REF_pubStatic、REF_invokeStatic
的方法控制程式碼,並且這個控制程式碼對應的類沒有初始化,會觸發其初始化。(黑人問號。。)
被動引用的例子
- 用子類引用父類的靜態欄位
SubClass.value
(value在父類中),只會初始化父類。 - 通過陣列來定義引用類,不會觸發次類的初始化
final
修飾的靜態常量會存入常量池,引用它不會觸發初始化(可以與上面的第一種情況比較)
與介面初始化的比較
- 介面不能使用static{}語句塊,但有
<clinit>()
類構造器 - 與第三條不同:初始化一個介面時,如果父類介面沒有初始化,父類介面不會初始化。
類載入的過程
載入
在載入期間虛擬機器完成以下三件事:
- 通過類的全限定名獲取定義此類的二進位制位元組流
- 通過這個二進位制流代表的靜態儲存結構轉化為方法區的執行時資料
- 在記憶體中生成一個代表這個類的
java.lang.Class
物件,作為方法區這個類的各種資料的訪問入口(HotSpot虛擬機器中Class物件是 在方法區中)
驗證
- 檔案格式驗證
- 後設資料驗證
- 位元組碼驗證
- 符號引用驗證
準備
為類的變數(static修飾)分配記憶體並設定變數的初始值。這些變數所用的記憶體都在方法區中分配。
注意:這裡的初始值是其預設值。如果是static final
修飾,就初始化為指定值,存在常量區。
解析
將虛擬機器中的符號引用替換為直接引用
主要的解析動作
- 類或者介面的解析
- 欄位解析
- 類方法的解析
- 介面方法的解析
初始化
執行<clinit>()
方法的過程。和<init>()
比較下:
<clinit>()
:類的初始化,類變數的賦值動作和靜態語句塊合併一起。<init>()
:類的例項化,也就是類的構造方法。初始化例項用的。
一些細節:
- 定義在靜態語句塊後面的變數,靜態語句塊可以賦值但不能訪問。如果訪問會報錯“非法前向引用”。
public class Clinit {
static {
i = 0;
// System.out.println(i); // 非法前向引用
}
static int i = 1;
}
複製程式碼
- 優先執行父類的
<clinit>()
方法 - 介面的父類
<clinit>()
方法不需要先執行,並且介面的實現類在初始化時也不會執行介面的<clinit>()
方法。 - 如果多個執行緒同時初始化一個類,只有一個執行緒會執行類的
<clinit>()
方法。
類載入器
類載入器乾的事:通過類的全限定名獲取定義此類的二進位制位元組流。只有被同一個類載入器所載入的類才有可能相等。
優勢:類層次劃分、OSGi、熱部署、程式碼加密 等等。
類載入器分類
- 啟動類載入器:c++實現,是虛擬機器等一部分,載入
<JAVA_HOME>/lib
目錄下的類 - 擴充套件類載入器:載入
<JAVA_HOME>/lib/ext
目錄下的類 - 應用程式類載入器:載入使用者路徑上指定的類庫
- 自定義類載入器:使用者自定義的類載入器
雙親委派模型
- 要求:除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。
- 過程:就是把載入的活推拖給父類載入器,一直推到最頂層。然後讓最頂層載入器載入,如果它幹不了,再遞給子,直到推給發起者,如果它也幹不了,就發出
ClassNotFoundException
異常。 - 優點:使類有類層次性。如在雙親委派模型下,Object類是由最頂層的啟動類載入器載入,所以它在每種載入器中都是同一個類,使Object這一最基礎的類的效能得以保證。