本來是想寫一篇關於Java類加載機制的博文,後來發現這個主題有點大,其中涉及的細節點太多,一篇博文,三言兩語恐怕沒法講明白,因而乎決定從總體到局部,先來談談類的生命週期,從總體把握一個類從「出生」到「凋亡」的過程,其中涉及了類加載、使用、卸載等各個階段,有了總體的認知後,再深刻細節並結合具體實例,探討加載原理、類加載器等相關知識。今天就讓博主帶領你開啓第一段旅程:類的生命週期詳解。java
類的生命週期是指一個class從加載到內存直至卸載出內存的過程,共包含加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段,以下圖所示:數組
其中驗證、準備、解析三個階段統稱爲鏈接(Linking),而加載、鏈接、初始化又能夠統稱爲類加載的過程,因此咱們有時又能夠稱類的生命週期包含加載、鏈接、初始化、使用和卸載這5個階段,或者是類加載、使用、卸載這3個階段。安全
回到上圖,加載、驗證、準備、初始化和卸載這5個階段的開始順序是肯定的,如圖中箭頭所示。之因此強調「開始順序」,是由於這裏的前後順序僅僅是各階段開始時間的順序,而不是進行或完成的順序,這些階段一般是相互交叉地混合式進行的。好比加載和驗證,並非說非要等到加載完成以後,纔開始驗證階段,在加載的階段中,會穿插各類檢驗動做,不然對於連格式都不符合的字節流,又怎能正確解析出其中的靜態數據結構從而轉化爲方法區中的數據結構呢?對於解析階段,其開始時間則比較特殊,既可能在加載階段就開始(對常量池中的符號引用的解析),也可能在初始化階段以後纔開始(支持Java語言的動態綁定)。服務器
下面咱們就來看看各個階段都大體作哪些事情。網絡
類加載的過程包含加載、鏈接和初始化三個階段。數據結構
加載是類加載過程的第一階段,此時虛擬機將查找並加載類的二進制數據,具體分爲三個步驟:多線程
這三條屬於虛擬機規範的內容,只指明瞭作什麼,具體實現交由虛擬機實現自行安排,這就給了虛擬機實現和具體應用足夠的靈活度。對於第一條,並未指明定義類的二進制字節流的存儲形式(class文件、ZIP包)、來源(本地文件系統、內存或網絡)以及獲取方式(既能夠從已有靜態資源讀取也可動態生成),於是就有了以下的多樣可能性:框架
對於第三條中所說的「內存」,虛擬機規範並無明確規定是在Java堆仍是方法區中,對於咱們最爲熟悉的HotSpot虛擬機,是存放在Java堆的永久代中。實際上永久代是HotSpot虛擬機特有的,是它對虛擬機規範中方法區概念的具體實現(JDK1.7及如下),對於其餘虛擬機(如IBM J9)是不存在永久代一說的,關於方法區和永久代的關係超出本博文的談論範疇了,點到爲止。佈局
加載階段完成後,本來定義類的二進制字節流就按照虛擬機所需的格式存儲在方法區中,這裏的存儲格式依具體的虛擬機實現而定,各有差別,虛擬機規範並未規定此區域的具體數據結構。優化
關於加載階段的注意點:
org.sherlockyb.test.HelloWorld
,定義一維數組類HelloWorld[] hws = new HelloWorld[8]
,虛擬機會直接建立名爲「[Lorg.sherlockyb.test.HelloWorld」的數組類,並對其進行初始化。上一節中加載階段的第一步驟——「經過一個類的全限定名來獲取定義此類的二進制字節流」,就是類加載器所作的惟一工做,類加載器是Java技術體系中的重要基石,它在類層次劃分、OSGi、熱部署、代碼加密等領域扮演着重要角色,關於它咱們暫且不作細緻介紹,後面會有單獨博文深刻探討之。
虛擬機規範並未強制規定加載階段具體何時開始,由虛擬機實現自由把握。就咱們所熟知的HotSpot虛擬機來講,有兩種狀況:
鏈接可細分爲三個階段:驗證、準備和解析。
鏈接的第一個階段,確保從class文件中所加載的字節流符合當前虛擬機的要求,且不會危害虛擬機自身的安全。該階段會依次進行以下校驗:
從上面能夠看出,驗證階段很是重要,關乎虛擬機的安全,但它並非必須的,它對程序運行期沒有影響,若是所引用的類已被反覆使用和驗證過,那麼能夠考慮採用-Xverifynone
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。一般來說,應用所加載的class文件都是由咱們本地或服務器的JDK編譯經過的,咱們都肯定它是符合虛擬機要求的,對於這類class文件其實並不須要驗證,主要是像從網絡加載的class字節流或是經過動態字節碼技術生成的字節流,出於安全的考慮,是必需要通過嚴格驗證的。
準備階段作的惟一一件事就是爲類的靜態變量分配內存,並將其初始化爲默認值。注意這裏的初始化和後面要講的「初始化階段」是不一樣的,容易混淆。這些內存都在方法區中分配。幾點注意項:
public static final int len = 5
,在準備階段len
的值已經被設置爲5了。實際上對於final的類變量,在編譯時就已經將其結果放入了調用它的類的常量池中,這種類變量的訪問並不會觸發其所屬類的初始化階段。該階段把類在常量池中的符號引用轉爲直接引用。符號引用就是一組用來描述目標的字面量,說白了就是靜態的佔位符,與內存佈局無關,而直接引用則是運行時的,是指內存中直接指向目標的指針、相對偏移量或間接定位到目標的句柄。解析工做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符這7類符號引用,將其替換爲直接引用。
虛擬機規範規定,在執行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic這16個用於操做符號引用的字節碼指令以前,必須先對符號引用進行解析。至於具體時間並未要求,交由虛擬機實現自行決定:在類被加載時就對常量池中的符號引用進行解析(靜態指令,除invokedynamic以外的),或是等到一個符號引用將要被使用前纔去解析(動態指令:invokedynamic,爲了支持動態綁定)。
爲類的靜態變量賦予程序設定的初始值。在Java中對類變量設定初始值有兩種方式:聲明類變量時指定初始值和靜態代碼塊爲靜態變量賦值。咱們來看下類的初始化步驟:
咱們能夠從字節碼層面獲知上述初始化步驟的原理,
編譯器在編譯Java源文件時,自動收集類中全部類變量的賦值操做和靜態語句塊中的語句(按照源碼中聲明前後順序),將其合併產生**<clinit>方法,即類構造器**(注意與實例構造器**<init>**相區分)。該方法的執行過程遵循如下規則:
虛擬機規範嚴格規定,當發生對一個類的主動引用時,會當即觸發類的初始化階段。主動引用有且僅有如下5種狀況:
除此以外,其餘全部引用類的方式都屬於被動引用,不會觸發初始化。
包括主動引用和被動引用,前者在上節已有說明,咱們來列舉幾個被動引用的實例:
A[] arr = new A[8]
,並不會觸發A的初始化。當一個類被斷定爲無用類時,才能夠被卸載。條件苛刻,須要同時知足以下條件:
對於知足上述3個條件的無用類,虛擬機能夠對其回收,但並非必然的,是否回收可經過-Xnoclassgc
參數控制。注意:在大量使用反射、動態代理等字節碼框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代(特指HotSpot虛擬機)不會溢出。
終於算是「蜻蜓點水」般地把Java類的生命週期過了一遍,相信當再提起類的生命週期時,你們腦海裏就會立馬浮現出類生命週期的大綱,都有哪些階段,每一個階段都大體作些什麼事情,都有些什麼注意點,這樣,本博文的目的就達到了!掌握了全局以後,接下來就是細節的探討,好比像驗證階段中的字節碼驗證,實際是很是複雜的,虛擬機專門爲此作了諸多優化;再好比解析階段,7類符號引用各自不一樣的解析細節又是什麼,等等之類。以後,筆者將會單獨另起博文,針對類加載器、解析階段等進行詳細分析,敬請期待。
同步更新到原文