Java 類加載過程
Class的生命週期
一個Class在虛擬機中的完整生命週期如下圖所示:
其中,驗證、整備、解析三個部分統稱爲連接。
需要說明的是,上述的流程只是描述了邏輯上各個階段的開始順序,實際過程中,各個階段可能是交錯進行,並不是一個階段等到另一個階段完全完成纔開始執行。
加載
加載一個Class需要完成以下3件事:
- 通過Class的全限定名獲取Class的二進制字節流
- 將Class的二進制內容加載到虛擬機的方法區
- 在內存中生成一個java.lang.Class對象表示這個Class
獲取Class的二進制字節流這個步驟有多種方式:
- 從zip中讀取,如:從jar、war、ear等格式的文件中讀取Class文件內容
- 從網絡中獲取,如:Applet
- 動態生成,如:動態代理、ASM框架等都是基於此方式
- 由其他文件生成,典型的是從jsp文件生成相應的Class
校驗
驗證一個Class的二進制內容是否合法,主要包括4個階段:
- 文件格式驗證,確保文件格式符合Class文件格式的規範。如:驗證魔數、版本號等。
- 元數據驗證,確保Class的語義描述符合Java的Class規範。如:該Class是否有父類、是否錯誤繼承了final類、是否一個合法的抽象類等。
- 字節碼驗證,通過分析數據流和控制流,確保程序語義符合邏輯。如:驗證類型轉換是合法的。
- 符號引用驗證,發生於符號引用轉換爲直接引用的時候(轉換髮生在解析階段)。如:驗證引用的類、成員變量、方法的是否可以被訪問(IllegalAccessError),當前類是否存在相應的方法、成員等(NoSuchMethodError、NoSuchFieldError)。
準備
在準備階段,虛擬機會在方法區中爲Class分配內存,並設置static成員變量的初始值爲默認值。注意這裏僅僅會爲static變量分配內存(static變量在方法區中),並且初始化static變量的值爲其所屬類型的默認值。如:int類型初始化爲0,引用類型初始化爲null。即使聲明瞭這樣一個static變量:
public static int a = 123;
在準備階段後,a在內存中的值仍然是0, 賦值123這個操作會在中初始化階段執行,因此在初始化階段產生了對應的Class對象之後a的值纔是123 。
解析
解析階段,虛擬機會將常量池中的符號引用替換爲直接引用,解析主要針對的是類、接口、方法、成員變量等符號引用。在轉換成直接引用後,會觸發校驗階段的符號引用驗證,驗證轉換之後的直接引用是否能找到對應的類、方法、成員變量等。這裏也可見類加載的各個階段在實際過程中,可能是交錯執行。
初始化
初始化階段即開始在內存中構造一個Class對象來表示該類,即執行類構造器<clinit>()
的過程。需要注意下,<clinit>()
不等同於創建類實例的構造方法<init>()
<clinit>()
方法中執行的是對static變量進行賦值的操作,以及static語句塊中的操作。- 虛擬機會確保先執行父類的
<clinit>()
方法。 - 如果一個類中沒有static的語句塊,也沒有對static變量的賦值操作,那麼虛擬機不會爲這個類生成
<clinit>()
方法。 -
虛擬機會保證
<clinit>()
方法的執行過程是線程安全的。
因此,存在如下一種最簡單的單例模式的實現:public class Singleton { public static final INSTANCE = new Singleton(); private Singleton() { } }