淺談一個Java類的生命週期

前言

一個Java類從被加載到虛擬機內存開始,到卸載出內存爲止,它經過了哪些步驟呢?這篇文章就來簡述一下關於Java類生命週期相關的知識,其中每個生命週期的具體內容不會細講,因爲內容太多,我準備專門花一篇文章介紹類生命週期中的詳細步驟,期待下一篇文章吧~

概述

一個Java類從開始到結束整個生命週期會經歷7個階段:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)。其中驗證、準備、解析三個部分又統稱爲連接(Linking)。

這裏我所說的Java類是已經編譯好的類,也就是說它已經是class字節碼了,如果要從.java文件算起的話應該還有個編譯過程。

每個階段的順序

並不是所有時候這七個階段都是順序進行的,其中加載、驗證、準備、初始化、卸載是固定順序開始的,解析階段不一定。解析在某些情況下可以在初始化階段之後再開始,這也是爲了支持運行時綁定(也成爲動態綁定)。剛剛說的五個階段是固定順序開始,但是不一定會按部就班地“進行”或“完成”,是因爲這些階段通常是互相交叉地混合進行的,通常會在一個階段執行的過程中調用激活另一個階段。

簡述七個階段

這裏先簡單介紹一下各個階段所做的事,每個階段詳細的過程在後面會有專門的文章介紹。

  1. 加載:加載過程就是把class字節碼文件載入到虛擬機中,至於從哪兒加載,虛擬機設計者並沒有限定,你可以從文件、壓縮包、網絡、數據庫等等地方加載class字節碼。
  • 通過類的全限定名來獲取定義此類的二進制字節流
  • 將此二進制字節流所代表的靜態存儲結構轉化成方法區的運行時數據結構
  • 在內存中生成代表此類的java.lang.Class對象,作爲該類訪問入口.
  1. 驗證:驗證的目的是確保class文件的字節流中信息符合虛擬機的要求,不會危害虛擬機安全,使得虛擬機免受惡意代碼的攻擊,這一步至關重要。
  • 文件格式驗證
  • 源數據驗證
  • 字節碼驗證
  • 符號引用驗證
  1. 準備:準備階段的工作就是爲類的靜態變量分配內存並設爲jvm默認的初值,對於非靜態的變量,則不會爲它們分配內存。靜態變量的初值爲jvm默認的初值,而不是我們在程序中設定的初值。(僅包含類變量,不包含實例變量).

  2. 解析:虛擬機將常量池中的符號引用替換爲直接引用,解析動作主要針對類或接口,字段,類方法,方法類型等等。

  3. 初始化:在該階段,才真正意義上的開始執行類中定義的java程序代碼,該階段會執行類構造器,並且在Java虛擬機規範中有明確的規定,在下面5種情況下必須對類進行初始化:

  • 遇到new、getstatic、putstatic、invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 使用java.long.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛擬機啓動時,需要制定一個執行的主類(即main方法的類),虛擬機必須先初始化這個類。
  • 使用動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行初始化,則需要先觸發其初始化。
  1. 使用:使用該類所提供的功能,其中包括主動引用和被動引用。

主動引用:

  • 通過new關鍵字實例化對象、讀取或設置類的靜態變量、調用類的靜態方法。
  • 通過反射方式執行以上三種行爲。
  • 初始化子類的時候,會觸發父類的初始化。
  • 作爲程序入口直接運行時(也就是直接調用main方法)。

被動引用:

  • 引用父類的靜態字段,只會引起父類的初始化,而不會引起子類的初始化。
  • 定義類數組,不會引起類的初始化。
  • 引用類的常量,不會引起類的初始化。
  1. 卸載:從內存中釋放,在我之前寫的垃圾回收機制(GC)總結一文中有介紹到方法區內存回收中對類的回收條件,這裏再貼出來一下:
  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
  • 加載該類的ClassLoader已經被回收;
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

引用類時容易忽略的點

在平時做面試題中很有可能會考察對類加載流程的理解,有的是直接給你幾個描述讓你選擇,有的是給出一段代碼,讓你判斷輸出結果。第一種方式偏向於理論,相信看了本文上面的介紹大多都知道多多少少,第二種往往是很多Java程序員容易犯錯的,接下來給出幾段代碼來講解。(環境是jdk1.8)

No.1

上面的代碼執行完成之後除了123被輸出外,在此之前還輸出了“父類被初始化”,並沒有輸出子類被初始化。對於靜態字段,只有直接定義這個字段的類纔會被初始化,這裏通過子類來訪問父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。這裏可以開啓-XX:+TraceClassLoading虛擬機參數查看加載內容。

No.2

上面的代碼執行完之後並沒有任何輸出!也就是說此過程並沒有觸發jvm.FatherClass的初始化階段,但是實際上這個過程觸發了另一個名爲[Lorg.FatherClass的類的初始化,它是一個由虛擬機自動生成的、直接繼承於Object的子類,創建動作由字節碼指令anewarray觸發。

[Lorg.FatherClass這個類代表了一個元素類型爲jvm.FatherClass的一維數組,數組中應有的屬性和方法(用戶可直接使用的只有被修飾爲public的length屬性和clone方法)都實現在這個類裏。

No.3

上面的代碼執行結束之後控制檯只輸出了123,並沒有輸出“父類被初始化”,說明此時FatherClass並沒有被觸發初始化,這裏和No.1裏面value唯一不同就在於多加了一個final關鍵字。這是因爲在編譯階段以及做了常量傳播優化,在編譯時就將常量value存儲到了Test類的常量池中,這裏對value的引用其實就是對本類(Test)常量池的引用,所以這裏無需初始化FatherClass類。也就是說,實際上Test的class文件之中並沒有FatherClass類的符號引用入口,這兩個類在編譯成class之後就不存在任何聯繫了。

爲了佐證該過程,我反編譯出No.1和No.3兩個測試代碼的Test字節碼文件

No.1情況下的Test.class



No.3情況下的Test.class



從字節碼中不難看出No.1情況獲取value值是通過getstatic #3獲取,其中#3是#3 = Fieldref #18.#19 // jvm/FatherClass.value:I,也就是說它仍然需要從FatherClass的引用獲取vlaue。而No.3情況獲取value值是bipush 123,這個123是直接從常量池中取的,無需從FatherClass類中獲取。

筆者也建立的自己的公衆號啦,平時會分享一些編程知識,歡迎各位大佬支持~

掃碼或微信搜索北風IT之路關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章