C/C++等純編譯語言從源碼到最終執行通常要經歷:編譯、鏈接和運行三個階段,鏈接是在編譯期間完成,而java在編譯期間僅僅是將源碼編譯爲Java虛擬機能夠識別的字節碼Class類文件,Java虛擬機對中Class類文件的加載、鏈接都在運行時執行,雖然類加載和鏈接會佔用程序的執行時間增長性能開銷,可是卻能夠爲java語言帶來高度靈活性和擴展性,java的針對接口編程和類加載器機制實現的OSGi以及熱部署等就是利用了運行時類加載和鏈接的特性,java的Class類在虛擬機中的生命週期以下:
java
上圖中加載、驗證、準備、初始化和卸載這個五個階段的順序是肯定的,而解析階段則不必定,在某些狀況下爲了支持java語言的運行時動態綁定,也能夠在初始化階段以後再開始。編程
(1).加載:緩存
Java虛擬機把Class類文件加載到內存中,並對Class文件中的數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的java類型的過程。安全
在加載階段,java虛擬機須要完成如下3件事:性能優化
a.經過一個類的全限定名來獲取定義此類的二進制字節流。數據結構
b.將定義類的二進制字節流所表明的靜態存儲結構轉換爲方法區的運行時數據結構。多線程
c.在java堆中生成一個表明該類的java.lang.Class對象,做爲方法區數據的訪問入口。jvm
加載階段與鏈接階段是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,這些夾在加載階段之中進行的動做仍然屬於鏈接階段,加載和鏈接階段仍然保持着固定的前後順序。佈局
(2).驗證:性能
驗證是鏈接階段的第一步,其目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機的安全,若是驗證失敗,會拋出java.lang.VerifyError異常。
驗證階段的主要工做有:
a.文件格式驗證:驗證Class文件魔數、主次版本、常量池、類文件自己等等。
b.元數據驗證:主要是對字節碼描述的信息進行語義分析,包括是否有父類、是不是抽象類、是不是接口、是否繼承了不容許被繼承的類(final類)、是否實現了父類或者接口的方法等等。
c.字節碼驗證:是整個驗證過程當中最複雜的,主要進行數據流和控制流分析,如保證跳轉指令不會跳轉到方法體以外的字節碼指令、數據類型轉換安全有效等。
d.符號引用驗證:發生在虛擬機將符號引用轉化爲直接引用的時候(鏈接第三階段-解析階段進行符號引用轉換爲直接引用),符號引用驗證的目的是確保解析動做能正常執行,若是沒法經過符號引用驗證,則會拋出java.lang.IncompatibleClassChangeError異常的子類異常,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
驗證階段對於虛擬機來講很是重要,可是不是一個必需的階段,若是所運行的代碼已經反覆被使用和驗證過了,能夠經過-Xverify:none參數關閉大部分的驗證措施,以提升虛擬機時間時間。
(3).準備:
準備階段是正式爲類變量(靜態變量,注意不是實例變量)分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。
對於普通非final的類變量,如public static int value = 123;在準備階段事後的初始值是0(數據類型的零值),而不是123,而把123賦值給value是在初始化階段才進行的動做。
對於final的類變量,即常量,如public staticfinal int value =123;在準備階段過程的初始值直接就是123了,不須要準備爲零值。
(4).解析:
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
符號引用(SymbolicReference):以一組符號來描述所引用的目標,與虛擬機內存佈局無關,引用的目標不必定已經被加載到虛擬機內存中。
直接引用(DirectReference):能夠直接指向目標的指針、相對偏移量或者是一個能間接定位到目標的句柄。直接引用和虛擬機實現的內存佈局相關,同一個符號引用在不一樣虛擬機上翻譯處理的直接引用不必定相同,若是有了直接引用,則引用的目標對象必須已經被加載到虛擬機內存中。
解析的動做主要針對類或接口、字段、類方法、接口方法四類符號引用進行解析。
(5).初始化:
初始化是類使用前的最後一個階段,在初始化階段java虛擬機真正開始執行類中定義的java程序代碼。
Java虛擬機規範嚴格規定了有且只有如下四種狀況必須當即對類進行初始化:
a.遇到new、獲取靜態變量(final常量除外)、爲靜態變量賦值以及調用靜態方法時,若是類沒有進行過初始化,則須要先觸發其初始化。
b.使用java.lang.reflect包的方法對類進行反射調用的時候(Class.forName(…)),若是類尚未初始化,須要先觸發對其的初始化。
c.當初始化一個類的時候,若是發現其父類尚未初始化,則須要先觸發對其父類的初始化。
d.當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
上述四種狀況稱爲對一個類的主動引用,除此以外的引用方式都不會觸發初始化,稱爲被動引用。
初始化的過程其實就是一個執行類構造器<clint>方法的過程,類構造器執行的特色和注意事項:
1).類構造器<clint>方法是由編譯器自動收集類中全部類變量(靜態非final變量)賦值動做和靜態初始化塊(static{……})中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定。靜態初始化塊中只能訪問到定義在它以前的類變量,定義在它以後的類變量,在前面的靜態初始化中能夠賦值,可是不能訪問。
2).類構造器<clint>方法與實例構造器<init>方法不一樣,它不須要顯式地調用父類構造器方法,虛擬機會保證在調用子類構造器方法以前,父類的構造器<clinit>方法已經執行完畢。
3).因爲父類構造器<clint>方法先與子類構造器執行,所以父類中定義的靜態初始化塊要先於子類的類變量賦值操做。
4). 類構造器<clint>方法對於類和接口並非必須的,若是一個類中沒有靜態初始化塊,也沒有類變量賦值操做,則編譯器能夠不爲該類生成類構造器<clint>方法。
5).接口中不能使用靜態初始化塊,但能夠有類變量賦值操做,所以接口與類同樣均可以生成類構造器<clint>方法。
接口與類不一樣的是:
首先,執行接口的類構造器<clint>方法時不須要先執行父接口的類構造器<clint>方法,只有當父接口中定義的靜態變量被使用時,父接口才會被初始化。
其次,接口的實現類在初始化時一樣不會執行接口的類構造器<clint>方法。
6).java虛擬機會保證一個類的<clint>方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,只會有一個線程去執行這個類的<clint>方法,其餘線程都須要阻塞等待,直到活動線程執行<clint>方法完畢。
初始化階段,當執行完類構造器<clint>方法以後,纔會執行實例構造器的<init>方法,實例構造方法一樣是按照先父類,後子類,先成員變量,後實例構造方法的順序執行。
(6).使用:
當初始化完成以後,java虛擬機就能夠執行Class的業務邏輯指令,經過堆中java.lang.Class對象的入口地址,調用方法區的方法邏輯,最後將方法的運算結果經過方法返回地址存放到方法區或堆中。
(7).卸載:
當對象再也不被使用時,java虛擬機的垃圾收集器將會回收堆中的對象,方法區中再也不被使用的Class也要被卸載,不然方法區(Sun HotSpot永久代)會內存溢出。
Java虛擬機規定只有當加載該類型的類加載器實例爲unreachable狀態時,當前被加載的類型才被卸載.啓動類加載器實例永遠爲reachable狀態,由啓動類加載器加載的類型可能永遠不會被卸載,類型卸載僅僅是做爲一種減小內存使用的性能優化措施存在的,具體和虛擬機實現有關,對開發者來講是透明的.
卸載自定義來加載器加載的類的可靠作法爲:
a.每次建立特定類加載器的新實例來加載指定類型的不一樣版本,這種使用場景下,通常就要犧牲緩存特定類型的類加載器實例以帶來性能優化的策略了.
b.對於指定類型已經被加載的版本, 會在適當時機達到unreachable狀態,被unload並垃圾回收.每次使用完類加載器特定實例後(肯定不須要再使用時), 將其顯示賦爲null, 這樣可能會比較快的達到jvm 規範中所說的類加載器實例unreachable狀態, 增大已經再也不使用的類型版本被儘快卸載的機會.