本篇中會對涉及到的知識點皆做出描述:
首先,我們先了解先虛擬機的類加載機制:
虛擬機把描述類的數據從Class 文件中加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的 java 類型,這便是虛擬機的類加載機制。
也就是說,一個文本.java 文件要運行起來:它首先要經過編譯成為 Class 文件(字節碼文件),然后被虛擬機加載讀入內存,接着虛擬機首先對其中的數據進行校驗
一個類從被加載的虛擬機內存中開始到卸載出內存為止(java 類的卸載並不能人為主動卸載,只能通過 JVM 的垃圾回收來卸載。沒有像 C 中析構函數那樣的東西)經歷的有:
加載
加載階段虛擬機完成三件事情:
1.通過一個類的全限定名稱來獲取定義此類的二進制字節流
2.將這個字節流所代表的靜態儲存結構轉化為方法區的運行時數據結構。
3.在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口
驗證
驗證階段大致完成4個階段的檢驗動作:
1.文件格式驗證
該驗證階段時基於二進制字節流的,通過這個階段驗證后字節流才會進入內存的方法區中進行存儲。而后面的3個驗證階段全部是基於方法區的存儲結構進行的。
2.元數據驗證:比如校驗類的繼承關系是否合法
3.字節碼驗證:例如保證跳轉指令不會跳轉到方法體之外的字節碼指令上。
4.符號引用驗證:例如符號引用中通過字符串描述的全限定名稱是否能找到對應的類
准備
正式為類變量分配內存並設置類變量初始值,變量所使用的內存都將在方法區中進行分配。(這個時候進行內存分配的僅包括類變量既被 static 修飾的變量而不包括實例變量。實例變量會在對象實例話時隨着對象一起分配在 java 堆中)
這時分配的初始值往往都時零值,實際代碼中定義的值在初始化階段才會賦予。(final修飾的除外,會在此階段賦值)
解析
解析階段是將常量池內的符號引用替換為直接引用的過程。
1.類或接口的解析
比如當前所在A類,在A類中引用了B類且B類從未被解析過。
(1)如果B不是一個數組類型,那虛擬機會把代表B的全限定名傳遞給A類的加載器去加載類B。
(2)如果B是一個數組類型,並且數組的元素為對象。那么虛擬機會按(1)中的方式去加載數組的元素類型。接着虛擬機會生成一個代表此數組維度和元素的數組對象。
(3)如果上面的步驟沒有異常,那么B在虛擬機中實際上已經成為一個有效的類或接口了,但在解析完成之前還需進行符號引用驗證,確認A是否具備對B的訪問權限。如不具備則拋出java.lang.IllegalAccessError 非法訪問異常。
2.字段解析
(1)如果A類本身包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
(2)否則,如果在A中實現了接口,將會按照繼承關系從下往上遞歸搜索各個接口的它的的父接口,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
(3)否則,如果A不是java.lang.Object的話,將會按照繼承關系從下往上遞歸搜索其父類。如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回字段查找結束。
(4)否則,查找失敗。
如果查找成功了還會對該字段進行權限驗證,如發現不具備對字段的訪問權限,將拋出java.lang.IllegalAccessError異常。
3.類方法解析
4.接口方法解析
初始化
進行初始化的條件:
1.當遇到 new\getstatic\putstatic\或invokestatic這4調字節碼指令時,如沒有進行過初始化則需要出發初始化。
既 當你用 new 實例化一個對象時 讀取或者設置一個類的靜態字段時 (這里有一種靜態字段除外:用 final 修飾,既已在編譯期把結果放入常量池的靜態字段除外) 調用一個類的靜態方法的時候 (同樣,也是被 fianl 修飾的除外)
總結來說,第一點就是 當用 new 實例化一個對象時 以及 用到沒有被 final 修飾的類的靜態字段或方法時 就會觸發初始化
2.使用 反射包的方法對類進行反射調用的時候
這個更好理解,反射調用類的方法是需要類的實例的,得到類的實例便要經過初始化過程。
3.當初始化一個類時,如果發現其父類還未初始化,則要先觸發其父類的初始化(接口稍有區別,只有在真正使用到父類接口的時候才會去初始化)
4.當虛擬機啟動時會先初始化主類(既包含main() 方法的那個類)
5.當使用JDK1.7的動態語言支持時。如果 java.lang.incoke.MethodHandle 實例最后的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
(了解java.lang.incoke.MethodHandle)
有且只有這五種情況才會觸發初始化。這五種為主動引用。除此之外所有引用類的方式都不會觸發初始化,這些稱為被動引動。
一下為被動引用:
1.通過子類引用父類的靜態字段。
2.通過數組定義來引用類,不會觸發此類的初始化
3.常量在編譯階段存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。
卸載
這七個階段。其中,驗證、准備、解析三個部分又被稱為連接。
這幾個階段的順序:
首先 加載、驗證、准備、初始化和卸載這五個過程是確定的(使用不用多說)。
1.加載------>2.驗證------>3.准備------->............?初始化.........6.使用------->.7.卸載
既解析和初始化這兩個的順序是不確定的。某些情況下解析可以在初始化之后再開始,以支持java 的運行時綁定,既動態綁定。(而動態綁定最為容易想到的例子就是多態)所以,可以說java 多態也是因為解析步驟可以在初始化之后。
而確定的這五個過程也並不是等一個完成才進行下一個的,這些階段通常都是互相交叉混合的進行的,通常會在一個階段執行的過程中調用、激活另一個階段。