【Java基礎】Java對象的生命周期
一、概述
一個類通過編譯器將一個Java文件編譯為Class字節碼文件,然后通過JVM中的解釋器編譯成不同操作系統的機器碼。雖然操作系統不同,但是基于解釋器的虛擬機是相同的。java類的生命周期就是指一個class文件加載到類文件注銷整個過程。
一個java類的完整的生命周期會經歷加載-連接-初始化-使用-卸載五個階段,當然也有在加載或連接之后沒有被初始化就直接被使用的情況,類加載的過程如下圖:
二、加載(loading)
java類生命周期加載(loading)是有類加載器完成(類加載器分為:BootstrapClassLoader,ExtClassLoader,AppClassLoader),類的class二進制文件讀取到內存后,并將其保存到方法區內,然后就創建一個java.lang.Class類型的對象。類被加載入JVM中,同一個類只被載入一次。加載是類加載的第一個環節。
java生命周期加載(loading)階段主要做三件事
- 類加載器(ClassLoader)通過一個類的全稱獲取二進制Class文件,加載到JVM內存中(如果已經獲取過則直接返回其Class對象)。
- 將字節流所代表的靜態存儲結構轉化為JVM方法區的運行時數據結構。
- 在內存中生產一個代表此類的java.lang.Class的對象,作為訪問這個類的入口。
二、連接(Linking)
當類被加載之后,系統為了生成一個對應的java.lang.Class對象,接著將會進入連接階段,連接階段會負載把類的二進制數據合并到JVM的運行狀態中。類的連接可以分為如下三個階段:
-
驗證:確保java類型的數據格式正確并使用與jvm使用;驗證作為連接階段的第一個階段,這個階段確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
驗證包含一下內容:
- 文件格式驗證:如是否以魔數 0xCAFEBABE 開頭、主、次版本號是否在當前虛擬機處理范圍之內、常量合理性驗證等。此階段保證輸入的字節流能正確地解析并存儲于方法區之內,格式上符合描述一個 Java類型信息的要求;
- 元數據驗證:是否存在父類,父類的繼承鏈是否正確,抽象類是否實現了其父類或接口之中要求實現的所有方法,字段、方法是否與父類產生矛盾等。第二階段,保證不存在不符合 Java 語言規范的元數據信息;
- 字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。例如保證跳轉指令不會跳轉到方法體以外的字節碼指令上;
- 符號引用驗證:在解析階段中發生,保證可以將符號引用轉化為直接引用。
可以考慮使用
-Xverify:none
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。 -
準備 :為該類分配內存;就是為類的靜態變量分配內存并設為jvm默認的初值,對于非靜態的變量,則不會為它們分配內存。靜態變量的初值是由JVM自動分配初始值。
JVM默認的初值如下:
- 內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中;
- 設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值;
- 對基本數據類型來說,對于類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統會為其賦予默認的零值,而對于局部變量來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
- 對于同時被static和final修飾的常量,必須在聲明的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予默認零值。
- 如果在數組初始化時沒有對數組中的各元素賦值,那么其中的元素將根據對應的數據類型而被賦予默認的零值。
-
解析:解析階段就是虛擬機將常量池中的符號引用轉化為直接引用的過程。
-
符號引用
符號引用是一個字符串,它給出了被引用的內容的名字并且可能會包含一些其他關于這個被引用項的信息——這些信息必須足以唯一的識別一個類、字段、方法。這樣,對于其他類的符號引用必須給出類的全名。對于其他類的字段,必須給出類名、字段名以及字段描述符。對于其他類的方法的引用必須給出類名、方法名以及方法的描述符。
-
直接引用可以是以下三種情況
- 直接指向目標的指針(比如,指向“類型”【Class對象】、類變量、類方法的直接引用可能是指向方法區的指針)
- 相對偏移量(比如,指向實例變量、實例方法的直接引用都是偏移量)
- 個能間接定位到目標的句柄 直接引用是和虛擬機的布局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經被加載入內存中了
解析階段可能開始于初始化之前,也可能在初始化之后開始,虛擬機會根據需要來判斷,到底是在類被加載器加載時就對常量池中的符號引用進行解析(初始化之前),還是等到一個符號引用將要被使用前才去解析它(初始化之后)
解析主要分為兩種情況:
- 類或接口的解析:判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,從而進行不同的解析。
- 字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關系從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關系遞歸搜索其父類,直至查找結束。
解析階段的靜態綁定和動態綁定:
- 靜態綁定(static binding):也叫前期綁定,在程序執行前,該方法就能夠確定所在的類,此時由編譯器或其它連接程序實現,比如構造方法或者被static或final修飾的。
- 動態綁定(auto binding):也叫后期綁定,在運行時,虛擬機根據具體對象的類型進行綁定,或者說是只有對象在虛擬機中創建了之后,才能確定方法屬于哪一個對象,比如含有泛型的。
-
三、初始化(Initialization)
執行靜態變量的初始化和靜態Java代碼塊,并初始化已設置好的變量值。需要注意的是加載、驗證和裝備階段只會進行一次,而初始化是可以重復進行的。在準備階段,類變量已經被初始化過一次系統提供的默認值,而在初始化階段,則是根據java代碼中實際指定的值去初始化類變量和其它內容。
類的初始化即是執行類構造器<clinit>
()方法的過程,規則如下:
<clinit>
()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合并產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之后的變量,則只可以賦值,而不能訪問。- ()方法與實例構造器()方法(類的構造函數)不同,它不需要顯式地調用父類構造器,虛擬機會保證在子類的()方法執行之前,父類的()方法已經執行完畢。因此,在虛擬機中第一個被執行的()方法的類肯定是java.lang.Object
<clinit>
()方法對于類或接口來說并不是必須的,如果一個類中沒有靜態語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生成<clinit>
()方法。- 接口中不能使用靜態語句塊,但仍然有類變量(final static)初始化的賦值操作,因此接口與類一樣會生成()方法。但是接口魚類不同的是:執行接口的()方法不需要先執行父接口的()方法,只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也一樣不會執行接口的()方法。
- 虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖和同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執行()方法完畢。如果在一個類的()方法中有耗時很長的操作,那就可能造成多個線程阻塞,在實際應用中這種阻塞往往是很隱蔽的。
觸發對象初始化的場景:
- 通過new關鍵字實例化對象、讀取或設置類的靜態變量、調用類的靜態方法。
- 通過反射方式實例化對象,如
Class.forName
()方法。 - 初始化子類的時候,會觸發父類的初始化。
- 虛擬機啟動時,初始化一個執行主類(也就是直接調用main方法)。
- 使用(反)序列化機制創建對象。
- 使用
JDK7
的動態語言支持時,如果一個java.lang.invoke.MethodHandle
實例最后的解析結果REF_getStatic
、REF_putStatic
、RE_invokeStatic
的方法句柄,并且這個方法句柄對應的類沒有進行初始化,則需要先觸發其初始化。
初始化的原則: 按照順序自上而下運行類中的變量賦值語句和靜態語句,如果有父類,則首先按照順序依次遞歸運行父類中的變量賦值語句和靜態語句。
四、使用(Using)
當一個對象初始化完成后就生成了一個對象的實例。
- 訪問類變量和方法不需要實例化
- 靜態代碼塊只會被調用一次,而實例的代碼塊則是每次初始化調用一次
- 通過final修飾符可以防止類被繼承或者變量的值被修改
- 設置訪問權限限制其它對象的訪問
實例化一個類大概有四種途徑:
- New操作符;
- 調用Class或者Java.lang.reflect.Constructor對象的newInstance()方法;
- 調用任何現有對象的Clone()方法;
- 通過java.io.ObjectInputStream類的getObject()方法反序列化;
實例化步驟:
- 在堆中為保存對象的實例變量分配內存;
- 為實例變量初始化為默認的初始值;
- 為實例變量賦正確的初始值,有三種技術完成賦值:
- 如果對象是clone() 創建的,jvm把原實例變量中的值拷貝到新對象中;
- 如果是通過ObjectInputStream類的readObject()調用反序列化的,jvm從輸入流中讀取的值來初始化實例變量;
- jvm調用對象的實例化方法把對象的實例變量初始化為正確的初始值;
五、卸載(Unloading)
jvm實現必須具有某種自動堆存儲管理策略,大部分是使用垃圾收集器。如果類聲明了 void finalize()方法,垃圾收集器在釋放實例內存前會執行這個方法。垃圾收集器自動調用的finalize()方法拋出的任何異常都將被忽略。
從jvm中卸載類型,很多情況,jvm中類的生命周期和對象的生命周期很相似。jvm如何判斷動態裝載的類型是否仍然被程序使用,其判斷方式和判斷對象是否仍然被使用很相似。在類使用完之后如果滿足下面的情況,類就會被卸載:
- 該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例。
- 加載該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
如果以上三個條件全部滿足,jvm就會在方法區垃圾回收的時候對類進行卸載,類的卸載過程其實就是在方法區中清空類信息,java類的整個生命周期就結束了。如果使用啟動類裝載器裝載的類型永遠都是可觸及的,所以永遠不會被卸載。只有使用用戶定義的類裝載器裝載的類型才會變成不可觸及,才會被卸載。
六、總結
Java生命周期中,對象基本上都是在jvm的堆區中創建,在創建對象之前,會觸發類加載(加載、連接、初始化),當類初始化完成后,根據類信息在堆區中實例化類對象,初始化非靜態變量、非靜態代碼以及默認構造方法,當對象使用完之后會在合適的時候被jvm垃圾收集器回收。讀完本文后我們知道,對象的生命周期只是類的生命周期中使用階段的主動引用的一種情況(即實例化類對象)。而類的整個生命周期則要比對象的生命周期長的多。