以下圖所示,JVM類加載機制分爲五個部分:加載,驗證,準備,解析,初始化,下面咱們就分別來看一下這五個過程。html
加載是類加載過程當中的一個階段,這個階段會在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的入口。注意這裏不必定非得要從一個Class文件獲取,這裏既能夠從ZIP包中讀取(好比從jar包和war包中讀取),也能夠在運行時計算生成(動態代理),也能夠由其它文件生成(好比將JSP文件轉換成對應的Class類)。java
這一階段的主要目的是爲了確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。數據庫
準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這裏所說的初始值概念,好比一個類變量定義爲:編程
1數組 |
|
實際上變量v在準備階段事後的初始值爲0而不是8080,將v賦值爲8080的putstatic指令是程序被編譯後,存放於類構造器<client>方法之中,這裏咱們後面會解釋。
可是注意若是聲明爲:網絡
1數據結構 |
|
在編譯階段會爲v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將v賦值爲8080。jvm
解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是class文件中的:
等類型的常量。
下面咱們解釋一下符號引用和直接引用的概念:
初始化階段是類加載最後一個階段,前面的類加載階段以後,除了在加載階段能夠自定義類加載器之外,其它操做都由JVM主導。到了初始階段,纔開始真正執行類中定義的Java程序代碼。
初始化階段是執行類構造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變量的賦值操做和靜態語句塊中的語句合併而成的。虛擬機會保證<client>方法執行以前,父類的<client>方法已經執行完畢。p.s: 若是一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器能夠不爲這個類生成<client>()方法。
注意如下幾種狀況不會執行類初始化:
虛擬機設計團隊把加載動做放到JVM外部實現,以便讓應用程序決定如何獲取所需的類,JVM提供了3種類加載器:
JVM經過雙親委派模型進行類的加載,固然咱們也能夠經過繼承java.lang.ClassLoader實現自定義的類加載器。
當一個類加載器收到類加載任務,會先交給其父類加載器去完成,所以最終加載任務都會傳遞到頂層的啓動類加載器,只有當父類加載器沒法完成加載任務時,纔會嘗試執行加載任務。
採用雙親委派的一個好處是好比加載位於rt.jar包中的類java.lang.Object,無論是哪一個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不一樣的類加載器最終獲得的都是一樣一個Object對象。
在有些情境中可能會出現要咱們本身來實現一個類加載器的需求,因爲這裏涉及的內容比較普遍,我想之後單獨寫一篇文章來說述,不過這裏咱們仍是稍微來看一下。咱們直接看一下jdk中的ClassLoader的源碼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
而上面的findClass()的實現以下,直接拋出一個異常,而且方法是protected,很明顯這是留給咱們開發者本身去實現的,這裏咱們之後咱們單獨寫一篇文章來說一下如何重寫findClass方法來實現咱們本身的類加載器。
1 2 3 |
|
看到這個題目,不少人會以爲我寫個人java代碼,至於類,JVM愛怎麼加載就怎麼加載,博主有很長一段時間也是這麼認爲的。隨着編程經驗的日積月累,愈來愈感受到了解虛擬機相關要領的重要性。閒話很少說,老規矩,先來一段代碼吊吊胃口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
運行結果:
1 2 3 |
|
答案答對了嚒?
也許有人會疑問:爲何沒有輸出SubClass init。ok~解釋一下:對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
上面就牽涉到了虛擬機類加載機制。若是有興趣,能夠繼續看下去。
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱爲鏈接(Linking)。如圖所示。
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。如下陳述的內容都已HotSpot爲基準。
在加載階段(能夠參考java.lang.ClassLoader的loadClass()方法),虛擬機須要完成如下3件事情:
加載階段和鏈接階段(Linking)的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。
驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體會完成4個階段的檢驗動做:
驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量的定義爲:
1 |
|
那變量value在準備階段事後的初始值爲0而不是123.由於這時候還沒有開始執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,因此把value賦值爲123的動做將在初始化階段纔會執行。
至於「特殊狀況」是指:public static final int value=123,即當類字段的字段屬性是ConstantValue時,會在準備階段初始化爲指定的值,因此標註爲final以後,value的值在準備階段初始化爲123而非0.
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
類初始化階段是類加載過程的最後一步,到了初始化階段,才真正開始執行類中定義的java程序代碼。在準備極端,變量已經付過一次系統要求的初始值,而在初始化階段,則根據程序猿經過程序制定的主管計劃去初始化類變量和其餘資源,或者說:初始化階段是執行類構造器<clinit>()方法的過程.
<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。以下:
1 2 3 4 5 6 7 8 9 |
|
<clinit>()方法與實例構造器<init>()方法不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類<init>()方法執行以前,父類的<clinit>()方法方法已經執行完畢,回到本文開篇的舉例代碼中,結果會打印輸出:SSClass就是這個道理。
因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。
<clinit>()方法對於類或者接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生產<clinit>()方法。
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法。但接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有好事很長的操做,就可能形成多個線程阻塞,在實際應用中這種阻塞每每是隱藏的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
運行結果:(即一條線程在死循環以模擬長時間操做,另外一條線程在阻塞等待)
1 2 3 |
|
須要注意的是,其餘線程雖然會被阻塞,但若是執行<clinit>()方法的那條線程退出<clinit>()方法後,其餘線程喚醒以後不會再次進入<clinit>()方法。同一個類加載器下,一個類型只會初始化一次。
將上面代碼中的靜態塊替換以下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
運行結果:
1 2 3 4 5 |
|
虛擬機規範嚴格規定了有且只有5中狀況(jdk1.7)必須對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):
開篇已經舉了一個範例:經過子類引用付了的靜態字段,不會致使子類初始化。
這裏再舉兩個例子。
1. 經過數組定義來引用類,不會觸發此類的初始化:(SuperClass類已在本文開篇定義)
1 2 3 4 5 6 7 |
|
運行結果:(無)
2. 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
運行結果:hello world
附:昨天從論壇上看到一個例子,頗有意思,以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
問題是:請問輸出是什麼?