簡介
Java虛擬機通過裝載、連接和初始化一個類型,使該類型可以被正在運行的Java程序使用。
- 裝載:把二進制形式的Java類型讀入Java虛擬機中。
- 連接:把裝載的二進制形式的類型數據合并到虛擬機的運行時狀態中去。
- 驗證:確保Java類型數據格式正確并且適合于Java虛擬機使用。
- 準備:負責為該類型分配它所需內存。
- 解析:把常量池中的符號引用轉換為直接引用。(可推遲到運行中的程序真正使用某個符號引用時再解析)
- 初始化:為類變量賦適當的初始值
所有Java虛擬機實現必須在每個類或接口首次主動使用時初始化。以下六種情況符合主動使用的要求:
- 當創建某個類的新實例時(new、反射、克隆、序列化)
- 調用某個類的靜態方法
- 使用某個類或接口的靜態字段,或對該字段賦值(用final修飾的靜態字段除外,它被初始化為一個編譯時常量表達式)
- 當調用Java API的某些反射方法時。
- 初始化某個類的子類時。
- 當虛擬機啟動時被標明為啟動類的類。
除以上六種情況,所有其他使用Java類型的方式都是被動的,它們不會導致Java類型的初始化。
對于接口來說,只有在某個接口聲明的非常量字段被使用時,該接口才會初始化,而不會因為事先這個接口的子接口或類要初始化而被初始化。
父類需要在子類初始化之前被初始化,所以這些類應該被裝載了。當實現了接口的類被初始化的時候,不需要初始化父接口。然而,當實現了父接口的子類(或者是擴展了父接口的子接口)被裝載時,父接口也要被裝載。(只是被裝載,沒有初始化)
裝載
- 通過該類型的全限定名,產生一個代表該類型的二進制數據流。
- 解析這個二進制數據流為方法去內的內部數據結構。
- 創建一個表示該類型的
java.lang.Class
類的實例。
Java虛擬機在識別Java class文件,產生了類型的二進制數據后,Java虛擬機必須把這些二進制數據解析為與實現相關的內部數據結構。裝載的最終產品就是Class實例,它稱為Java程序與內部數據結構之間的接口。要訪問關于該類型的信息(存儲在內部數據結構中),程序就要調用該類型對應的Class實例的方法。這樣一個過程,就是把一個類型的二進制數據解析為方法區中的內部數據結構,并在堆上建立一個Class對象的過程,這被稱為"創建"類型。
驗證
確認裝載后的類型符合Java語言的語義,并且不會危及虛擬機的完整性。
-
裝載時驗證
:檢查二進制數據以確保數據全部是預期格式、確保除Object之外的每個類都有父類、確保該類的所有父類都已經被裝載。 -
正式驗證階段
:檢查final類不能有子類、確保final方法不被覆蓋、確保在類型和超類型之間沒有不兼容的方法聲明(比如擁有兩個名字相同的方法,參數在數量、順序、類型上都相同,但返回類型不同)。 -
符號引用的驗證
:當虛擬機搜尋一個唄符號引用的元素(類型、字段或方法)時,必須首先確認該元素存在。如果虛擬機發現元素存在,則必須進一步檢查引用類型有訪問該元素的權限。
準備
當Java虛擬機裝載一個類,并執行了一些驗證之后,類就可以進入準備階段。在準備階段,Java虛擬機為類變量分配內存,設置默認初始值。但在到到初始化階段之前,類變量都沒有被初始化為真正的初始值。
boolean在內部常常被實現為一個int,會被默認初始化為0。
解析
類型經過連接的前兩個階段--驗證和準備--之后,就可以進入第三個階段--解析。解析的過程就是在類型的常量池總尋找類、接口、字段和方法的符號引用,把這些符號引用替換為直接引用的過程。
類或接口的解析
:判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,從而進行不同的解析。字段解析
:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關系從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關系從上往下遞歸搜索其父類,直至查找結束,
初始化
為類變量賦予“正確”的初始值。這里的“正確”的初始值是指程序員希望這個類變量所具備的初始值。所有的類變量(即靜態量)初始化語句和類型的靜態初始化器都被Java編譯器收集在一起,放到一個特殊的方法中。 對于類來說,這個方法被稱作類初始化方法;對于接口來說,它被稱為接口初始化方法。在類和接口的class文件中,這個方法被稱為<clinit>
。
初始化類的步驟:
- 如果存在直接父類,且直接父類沒有被初始化,先初始化直接父類。
- 如果類存在一個類初始化方法,執行此方法。
這個步驟是遞歸執行的,即第一個初始化的類一定是Object
。初始化接口并不需要初始化它的父接口。
Java虛擬機必須確保初始化過程被正確地同步。 如果多個線程需要初始化一個類,僅僅允許一個線程來進行初始化,其他線程需等待。
這個特性可以用來寫單例模式。
<clinit>()
方法
- 對于靜態變量和靜態初始化語句來說:執行的順序和它們在類或接口中出現的順序有關。
-
并非所有的類都需要在它們的
class
文件中擁有<clinit>()
方法, 如果類沒有聲明任何類變量,也沒有靜態初始化語句,那么它就不會有<clinit>()
方法。如果類聲明了類變量,但沒有明確的使用類變量初始化語句或者靜態代碼塊來初始化它們,也不會有<clinit>()
方法。如果類僅包含靜態final
常量的類變量初始化語句,而且這些類變量初始化語句采用編譯時常量表達式,類也不會有<clinit>()
方法。只有那些需要執行Java代碼來賦值的類才會有<clinit>()
-
final
常量:Java虛擬機在使用它們的任何類的常量池或字節碼中直接存放的是它們表示的常量值。
主動使用和被動使用
主動使用有六種情況,在前面已經寫過。
使用一個非常量的靜態字段只有當類或接口的確聲明了這個字段時才是主動使用。 比如:類中聲明的字段可能被子類引用;接口中聲明的字段可能被子接口或實現了這個接口的類引用。對于子類、子接口或實現了接口的類來說,這是被動使用--不會觸發它們的初始化。只有當字段的確是被類或接口聲明的時候才是主動使用。