【Android面試-Java-V04】Java類載入過程

tracyliu發表於2020-04-05

考察點:

  1. 程式碼執行順序
  2. 描述java的類載入的過程

執行順序(優先順序從高到底):

  • 靜態程式碼塊
  • main方法
  • 構造程式碼塊
  • 構造方法

其中靜態程式碼塊執行一次,構造程式碼塊每次建立都會執行

以上方法從父類到子類,依次執行。如A 的static method

範例:Person person = new Person();為例進行說明。

  • 查詢Person.class,並載入到記憶體中。
  • 執行類裡的靜態程式碼塊。
  • 在堆記憶體裡開闢記憶體空間,並分配記憶體地址。
  • 在堆記憶體裡建立物件的屬性,並進行預設的初始化。
  • 對屬性進行顯示初始化。
  • 對物件進行構造程式碼塊初始化。
  • 呼叫物件的建構函式進行初始化。
  • 將物件的地址賦值給person變數。

描述一個類的載入過程?Java虛擬機器通過裝載、連線和初始化一個型別,使該型別可以被正在執行的Java程式使用。

  1. 裝載:把二進位制形式的Java型別讀入Java虛擬機器中。
  2. 連線:把裝載的二進位制形式的型別資料合併到虛擬機器的執行時狀態中去。
  • 驗證:確保Java型別資料格式正確並且適合於Java虛擬機器使用。
  • 準備:負責為該型別分配它所需記憶體。
  • 解析:把常量池中的符號引用轉換為直接引用。(可推遲到執行中的程式真正使用某個符號引用時再解析)
  1. 初始化:為類變數賦適當的初始值

載入

  • 通過該型別的全限定名,產生一個代表該型別的二進位制資料流。
  • 解析這個二進位制資料流為方法去內的內部資料結構。
  • 建立一個表示該型別的java.lang.Class類的例項。

Java虛擬機器在識別Java class檔案,產生了型別的二進位制資料後,Java虛擬機器必須把這些二進位制資料解析為與實現相關的內部資料結構。裝載的最終產品就是Class例項,它稱為Java程式與內部資料結構之間的介面。要訪問關於該型別的資訊(儲存在內部資料結構中),程式就要呼叫該型別對應的Class例項的方法。這樣一個過程,就是把一個型別的二進位制資料解析為方法區中的內部資料結構,並在堆上建立一個Class物件的過程,這被稱為"建立"型別。

驗證

確認裝載後的型別符合Java語言的語義,並且不會危及虛擬機器的完整性。

  • 裝載時驗證:檢查二進位制資料以確保資料全部是預期格式、確保除Object之外的每個類都有父類、確保該類的所有父類都已經被裝載。
  • 正式驗證階段:檢查final類不能有子類、確保final方法不被覆蓋、確保在型別和超型別之間沒有不相容的方法宣告(比如擁有兩個名字相同的方法,引數在數量、順序、型別上都相同,但返回型別不同)。
  • 符號引用的驗證:當虛擬機器搜尋一個被符號引用的元素(型別、欄位或方法)時,必須首先確認該元素存在。如果虛擬機器發現元素存在,則必須進一步檢查引用型別有訪問該元素的許可權。

準備

當Java虛擬機器裝載一個類,並執行了一些驗證之後,類就可以進入準備階段。在準備階段,Java虛擬機器為類變數分配記憶體,設定預設初始值。但在到到初始化階段之前,類變數都沒有被初始化為真正的初始值。

boolean在內部常常被實現為一個int,會被預設初始化為0。

解析

型別經過連線的前兩個階段--驗證和準備--之後,就可以進入第三個階段--解析。解析的過程就是在型別的常量池總尋找類、介面、欄位和方法的符號引用,把這些符號引用替換為直接引用的過程

  • 類或介面的解析:判斷所要轉化成的直接引用是陣列型別,還是普通的物件型別的引用,從而進行不同的解析。

  • 欄位解析:對欄位進行解析時,會先在本類中查詢是否包含有簡單名稱和欄位描述符都與目標相匹配的欄位,如果有,則查詢結束;如果沒有,則會按照繼承關係從上往下遞迴搜尋該類所實現的各個介面和它們的父介面,還沒有,則按照繼承關係從上往下遞迴搜尋其父類,直至查詢結束,

初始化

為類變數賦予“正確”的初始值。這裡的“正確”的初始值是指程式設計師希望這個類變數所具備的初始值。所有的類變數(即靜態量)初始化語句和型別的靜態初始化器都被Java編譯器收集在一起,放到一個特殊的方法中。 對於類來說,這個方法被稱作類初始化方法;對於介面來說,它被稱為介面初始化方法。在類和介面的class檔案中,這個方法被稱為<clinit>

初始化類的步驟:

1. 如果存在直接父類,且直接父類沒有被初始化,先初始化直接父類。
2. 如果類存在一個類初始化方法,執行此方法。
複製程式碼

這個步驟是遞迴執行的,即第一個初始化的類一定是Object初始化介面並不需要初始化它的父介面。

Java虛擬機器必須確保初始化過程被正確地同步。 如果多個執行緒需要初始化一個類,僅僅允許一個執行緒來進行初始化,其他執行緒需等待。

這個特性可以用來寫單例模式。

<clinit>()方法

  • 對於靜態變數和靜態初始化語句來說:執行的順序和它們在類或介面中出現的順序有關。
  • 並非所有的類都需要在它們的class檔案中擁有<clinit>()方法, 如果類沒有宣告任何類變數,也沒有靜態初始化語句,那麼它就不會有<clinit>()方法。如果類宣告瞭類變數,但沒有明確的使用類變數初始化語句或者靜態程式碼塊來初始化它們,也不會有<clinit>()方法。如果類僅包含靜態final常量的類變數初始化語句,而且這些類變數初始化語句採用編譯時常量表示式,類也不會有<clinit>()方法。只有那些需要執行Java程式碼來賦值的類才會有<clinit>()
  • final常量:Java虛擬機器在使用它們的任何類的常量池或位元組碼中直接存放的是它們表示的常量值。

主動使用和被動使用

所有Java虛擬機器實現必須在每個類或介面首次主動使用時初始化。

  • 當建立某個類的新例項時(new、反射、克隆、序列化)
  • 呼叫某個類的靜態方法
  • 使用某個類或介面的靜態欄位,或對該欄位賦值(用final修飾的靜態欄位除外,它被初始化為一個編譯時常量表示式)
  • 當呼叫Java API的某些反射方法時。
  • 初始化某個類的子類時。
  • 當虛擬機器啟動時被標明為啟動類的類。

除以上六種情況,所有其他使用Java型別的方式都是被動的,它們不會導致Java型別的初始化。

對於介面來說,只有在某個介面宣告的非常量欄位被使用時,該介面才會初始化,而不會因為事先這個介面的子介面或類要初始化而被初始化。

使用一個非常量的靜態欄位只有當類或介面的確宣告瞭這個欄位時才是主動使用。 比如:類中宣告的欄位可能被子類引用;介面中宣告的欄位可能被子介面或實現了這個介面的類引用。對於子類、子介面或實現了介面的類來說,這是被動使用--不會觸發它們的初始化。只有當欄位的確是被類或介面宣告的時候才是主動使用。

相關文章