天天看點

Java 類的生命周期詳解

一、 引 言

        最近有位細心的朋友在閱讀筆者的文章時,對java類的生命周期問題有一些疑惑,筆者打開百度搜了一下相關的問題,看到網上的資料很少有把這個問題講明白的,主要是因為目前國内java方面的教材大多隻是告訴你“怎樣做”,但至于“為什麼這樣做”卻不多說,是以造成大家在基礎和原理方面的知識比較匮乏,是以筆者今天就鬥膽來講一下這個問題,權當抛磚引玉,希望對在這個問題上有疑惑的朋友有所幫助,文中有說的不對的地方,也希望各路高手前來指正。

        首先來了解一下jvm(java虛拟機)中的幾個比較重要的記憶體區域,這幾個區域在java類的生命周期中扮演着比較重要的角色:

  • 方法區:在java的虛拟機中有一塊專門用來存放已經加載的類資訊、常量、靜态變量以及方法代碼的記憶體區域,叫做方法區。
  • 常量池:常量池是方法區的一部分,主要用來存放常量和類中的符号引用等資訊。
  • 堆區:用于存放類的對象執行個體。
  • 棧區:也叫java虛拟機棧,是由一個一個的棧幀組成的後進先出的棧式結構,棧桢中存放方法運作時産生的局部變量、方法出口等資訊。當調用一個方法時,虛拟機棧中就會建立一個棧幀存放這些資料,當方法調用完成時,棧幀消失,如果方法中調用了其他方法,則繼續在棧頂建立新的棧桢。

        除了以上四個記憶體區域之外,jvm中的運作時記憶體區域還包括本地方法棧和程式計數器,這兩個區域與java類的生命周期關系不是很大,在這裡就不說了,感興趣的朋友可以自己百度一下。

二、類的生命周期

        當我們編寫一個java的源檔案後,經過編譯會生成一個字尾名為class的檔案,這種檔案叫做位元組碼檔案,隻有這種位元組碼檔案才能夠在java虛拟機中運作,java類的生命周期就是指一個class檔案從加載到解除安裝的全過程。

        一個java類的完整的生命周期會經曆加載、連接配接、初始化、使用、解除安裝五個階段,當然也有在加載或者連接配接之後沒有被初始化就直接被使用的情況,如圖所示:

Java 類的生命周期詳解

下面我們就依次來說一說這五個階段。

(1)加載

       在java中,我們經常會接觸到一個詞——類加載,它和這裡的加載并不是一回事,通常我們說類加載指的是類的生命周期中加載、連接配接、初始化三個階段。在加載階段,java虛拟機會做什麼工作呢?其實很簡單,就是找到需要加載的類并把類的資訊加載到jvm的方法區中,然後在堆區中執行個體化一個java.lang.Class對象,作為方法區中這個類的資訊的入口。

       類的加載方式比較靈活,我們最常用的加載方式有兩種,一種是根據類的全路徑名找到相應的class檔案,然後從class檔案中讀取檔案内容;另一種是從jar檔案中讀取。另外,還有下面幾種方式也比較常用:

  • 從網絡中擷取:比如10年前十分流行的Applet。
  • 根據一定的規則實時生成,比如設計模式中的動态代理模式,就是根據相應的類自動生成它的代理類。
  • 從非class檔案中擷取,其實這與直接從class檔案中擷取的方式本質上是一樣的,這些非class檔案在jvm中運作之前會被轉換為可被jvm所識别的位元組碼檔案。

       對于加載的時機,各個虛拟機的做法并不一樣,但是有一個原則,就是當jvm“預期”到一個類将要被使用時,就會在使用它之前對這個類進行加載。比如說,在一段代碼中出現了一個類的名字,jvm在執行這段代碼之前并不能确定這個類是否會被使用到,于是,有些jvm會在執行前就加載這個類,而有些則在真正需要用的時候才會去加載它,這取決于具體的jvm實作。我們常用的hotspot虛拟機是采用的後者,就是說當真正用到一個類的時候才對它進行加載。

       加載階段是類的生命周期中的第一個階段,加載階段之後,是連接配接階段。有一點需要注意,就是有時連接配接階段并不會等加載階段完全完成之後才開始,而是交叉進行,可能一個類隻加載了一部分之後,連接配接階段就已經開始了。但是這兩個階段總的開始時間和完成時間總是固定的:加載階段總是在連接配接階段之前開始,連接配接階段總是在加載階段完成之後完成。

(2)連接配接

       連接配接階段比較複雜,一般會跟加載階段和初始化階段交叉進行,這個階段的主要任務就是做一些加載後的驗證工作以及一些初始化前的準備工作,可以細分為三個步驟:驗證、準備、解析。

  1. 驗證:當一個類被加載之後,必須要驗證一下這個類是否合法,比如這個類是不是符合位元組碼的格式、變量與方法是不是有重複、資料類型是不是有效、繼承與實作是否合乎标準等等。總之,這個階段的目的就是保證加載的類是能夠被jvm所運作。
  2. 準備:準備階段的工作就是為類的靜态變量配置設定記憶體并設為jvm預設的初值,對于非靜态的變量,則不會為它們配置設定記憶體。有一點需要注意,這時候,靜态變量的初值為jvm預設的初值,而不是我們在程式中設定的初值。jvm預設的初值是這樣的:
    • 基本類型(int、long、short、char、byte、boolean、float、double)的預設值為0。
    • 引用類型的預設值為null。
    • 常量的預設值為我們程式中設定的值,比如我們在程式中定義final static int a = 100,則準備階段中a的初值就是100。
  3.  解析:這一階段的任務就是把常量池中的符号引用轉換為直接引用。那麼什麼是符号引用,什麼又是直接引用呢?我們來舉個例子:我們要找一個人,我們現有的資訊是這個人的身份證号是1234567890。隻有這個資訊我們顯然找不到這個人,但是通過警察局的身份系統,我們輸入1234567890這個号之後,就會得到它的全部資訊:比如安徽省黃山市餘暇村18号張三,通過這個資訊我們就能找到這個人了。這裡,123456790就好比是一個符号引用,而安徽省黃山市餘暇村18号張三就是直接引用。在記憶體中也是一樣,比如我們要在記憶體中找一個類裡面的一個叫做show的方法,顯然是找不到。但是在解析階段,jvm就會把show這個名字轉換為指向方法區的的一塊記憶體位址,比如c17164,通過c17164就可以找到show這個方法具體配置設定在記憶體的哪一個區域了。這裡show就是符号引用,而c17164就是直接引用。在解析階段,jvm會将所有的類或接口名、字段名、方法名轉換為具體的記憶體位址。

        連接配接階段完成之後會根據使用的情況(直接引用還是被動引用)來選擇是否對類進行初始化。

(3)初始化

       如果一個類被直接引用,就會觸發類的初始化。在java中,直接引用的情況有:

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

        除了以上四種情況,其他使用類的方式叫做被動引用,而被動引用不會觸發類的初始化。請看主動引用的示例代碼:

[java]  view plain copy

  1. import java.lang.reflect.Field;  
  2. import java.lang.reflect.Method;  
  3. class InitClass{  
  4.     static {  
  5.         System.out.println("初始化InitClass");  
  6.     }  
  7.     public static String a = null;  
  8.     public static void method(){}  
  9. }  
  10. class SubInitClass extends InitClass{}  
  11. public class Test1 {  
  12.     public static void main(String[] args) throws Exception{  
  13.     //  主動引用引起類的初始化一: new對象、讀取或設定類的靜态變量、調用類的靜态方法。  
  14.     //  new InitClass();  
  15.     //  InitClass.a = "";  
  16.     //  String a = InitClass.a;  
  17.     //  InitClass.method();  
  18.     //  主動引用引起類的初始化二:通過反射執行個體化對象、讀取或設定類的靜态變量、調用類的靜态方法。  
  19.     //  Class cls = InitClass.class;  
  20.     //  cls.newInstance();  
  21.     //  Field f = cls.getDeclaredField("a");  
  22.     //  f.get(null);  
  23.     //  f.set(null, "s");  
  24.     //  Method md = cls.getDeclaredMethod("method");  
  25.     //  md.invoke(null, null);  
  26.     //  主動引用引起類的初始化三:執行個體化子類,引起父類初始化。  
  27.     //  new SubInitClass();  
  28.     }  
  29. }  

        上面的程式示範了主動引用觸發類的初始化的四種情況。

        類的初始化過程是這樣的:按照順序自上而下運作類中的變量指派語句和靜态語句,如果有父類,則首先按照順序運作父類中的變量指派語句和靜态語句。先看一個例子,首先建兩個類用來顯示指派操作:

[java]  view plain copy

  1. public class Field1{  
  2.     public Field1(){  
  3.         System.out.println("Field1構造方法");  
  4.     }  
  5. }  
  6. public class Field2{  
  7.     public Field2(){  
  8.         System.out.println("Field2構造方法");  
  9.     }  
  10. }  

下面是示範初始化順序的代碼:

[java]  view plain copy

  1. class InitClass2{  
  2.     static{  
  3.         System.out.println("運作父類靜态代碼");  
  4.     }  
  5.     public static Field1 f1 = new Field1();  
  6.     public static Field1 f2;   
  7. }  
  8. class SubInitClass2 extends InitClass2{  
  9.     static{  
  10.         System.out.println("運作子類靜态代碼");  
  11.     }  
  12.     public static Field2 f2 = new Field2();  
  13. }  
  14. public class Test2 {  
  15.     public static void main(String[] args) throws ClassNotFoundException{  
  16.         new SubInitClass2();  
  17.     }  
  18. }  

        上面的代碼中,初始化的順序是:第03行,第05行,第11行,第13行。第04行是聲明操作,沒有指派,是以不會被運作。而下面的代碼:

[java]  view plain copy

  1. class InitClass2{  
  2.     public static Field1 f1 = new Field1();  
  3.     public static Field1 f2;  
  4.     static{  
  5.         System.out.println("運作父類靜态代碼");  
  6.     }  
  7. }  
  8. class SubInitClass2 extends InitClass2{  
  9.     public static Field2 f2 = new Field2();  
  10.     static{  
  11.         System.out.println("運作子類靜态代碼");  
  12.     }  
  13. }  
  14. public class Test2 {  
  15.     public static void main(String[] args) throws ClassNotFoundException{  
  16.         new SubInitClass2();  
  17.     }  
  18. }  

        初始化順序為:第02行、第05行、第10行、第12行,各位可以運作程式檢視結果。

       在類的初始化階段,隻會初始化與類相關的靜态指派語句和靜态語句,也就是有static關鍵字修飾的資訊,而沒有static修飾的指派語句和執行語句在執行個體化對象的時候才會運作。

(4)使用

       類的使用包括主動引用和被動引用,主動引用在初始化的章節中已經說過了,下面我們主要來說一下被動引用:

  • 引用父類的靜态字段,隻會引起父類的初始化,而不會引起子類的初始化。
  • 定義類數組,不會引起類的初始化。
  • 引用類的常量,不會引起類的初始化。

被動引用的示例代碼:

[java]  view plain copy

  1. class InitClass{  
  2.     static {  
  3.         System.out.println("初始化InitClass");  
  4.     }  
  5.     public static String a = null;  
  6.     public final static String b = "b";  
  7.     public static void method(){}  
  8. }  
  9. class SubInitClass extends InitClass{  
  10.     static {  
  11.         System.out.println("初始化SubInitClass");  
  12.     }  
  13. }  
  14. public class Test4 {  
  15.     public static void main(String[] args) throws Exception{  
  16.     //  String a = SubInitClass.a;// 引用父類的靜态字段,隻會引起父類初始化,而不會引起子類的初始化  
  17.     //  String b = InitClass.b;// 使用類的常量不會引起類的初始化  
  18.         SubInitClass[] sc = new SubInitClass[10];// 定義類數組不會引起類的初始化  
  19.     }  
  20. }  

        最後總結一下使用階段:使用階段包括主動引用和被動引用,主動飲用會引起類的初始化,而被動引用不會引起類的初始化。

        當使用階段完成之後,java類就進入了解除安裝階段。

(5)解除安裝

       關于類的解除安裝,筆者在單例模式讨論篇:單例模式與垃圾回收一文中有過描述,在類使用完之後,如果滿足下面的情況,類就會被解除安裝:

  • 該類所有的執行個體都已經被回收,也就是java堆中不存在該類的任何執行個體。
  • 加載該類的ClassLoader已經被回收。
  • 該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射通路該類的方法。

        如果以上三個條件全部滿足,jvm就會在方法區垃圾回收的時候對類進行解除安裝,類的解除安裝過程其實就是在方法區中清空類資訊,java類的整個生命周期就結束了。

三、總 結

        做java的朋友對于對象的生命周期可能都比較熟悉,對象基本上都是在jvm的堆區中建立,在建立對象之前,會觸發類加載(加載、連接配接、初始化),當類初始化完成後,根據類資訊在堆區中執行個體化類對象,初始化非靜态變量、非靜态代碼以及預設構造方法,當對象使用完之後會在合适的時候被jvm垃圾收集器回收。讀完本文後我們知道,對象的生命周期隻是類的生命周期中使用階段的主動引用的一種情況(即執行個體化類對象)。而類的整個生命周期則要比對象的生命周期長的多。

參考推薦:

詳解java類的生命周期

C語言編譯全過程剖析

Java 記憶體模型及GC原理

Android 智能指針原理