java類與對象生命週期

本文主要介紹:

             java類的生命週期;

             java對象的生命週期。

一、java類的生命週期

首先來了解一下jvm(java虛擬機)中的幾個比較重要的內存區域,這幾個區域在java類的生命週期中扮演着比較重要的角色:
方法區:在java的虛擬機中有一塊專門用來存放已經加載的類信息、常量、靜態變量以及方法代碼的內存區域,叫做方法區。
常量池:常量池是方法區的一部分,主要用來存放常量和類中的符號引用等信息。
堆區:用於存放類的對象實例。
棧區:也叫java虛擬機棧,是由一個一個的棧幀組成的後進先出的棧式結構,棧楨中存放方法運行時產生的局部變量、方法出口等信息。當調用一個方法時,虛擬機棧中就會創建       一個棧幀存放這些數據,當方法調用完成時,棧幀消失,如果方法中調用了其他方法,則繼續在棧頂創建新的棧楨。
除了以上四個內存區域之外,jvm中的運行時內存區域還包括本地方法棧和程序計數器,這兩個區域與java類的生命週期關係不是很大,在這裏就不說了。

類的生命週期
當我們編寫一個java的源文件後,經過編譯會生成一個後綴名爲class的文件,這種文件叫做字節碼文件,只有這種字節碼文件才能夠在java虛擬機中運行。java虛擬機將編譯生成的.class文件按照一定的需求和規則加載進內存,並組織成一個完整的應用程序。Java 語言把每個單獨的類 Class 和接口 Implements 編譯成單獨的一個 . class 文件,這些文件對於 Java 運行環境來說就是一個個可以動態加載的單元。正是因爲 Java 的這種特性,我們可以在不重新編譯其它代碼的情況下,只編譯需要修改的單元,並把修改文件編譯後的 . class 文件放到 Java 的路徑當中, 等到下次該 Java 虛擬機器重新激活時,這個邏輯上的 Java 應用程序就會因爲加載了新修改的 .class 文件,自己的功能也做了更新,這就是 Java 的動態性。
java類的生命週期就是指一個class文件從加載到卸載的全過程。一個java類的完整的生命週期會經歷加載、連接、初始化、使用、和卸載五個階段,當然也有在加載或者連接之後沒有被初始化就直接被使用的情況,如圖所示:


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

加載

(在java中,我們經常會接觸到一個詞——類加載,它和這裏的加載並不是一回事,通常我們說類加載指的是類的生命週期中加載、連接、初始化三個階段。)

在加載階段,java虛擬機會做什麼工作呢?其實很簡單,就是找到需要加載的類並把類的信息加載到jvm的方法區中,然後在堆區中實例化一個java.lang.Class對象,作爲方法區中這個類的信息的入口。
類的加載方式比較靈活,我們最常用的加載方式有兩種,一種是根據類的全路徑名找到相應的class文件,然後從class文件中讀取文件內容;另一種是從jar文件中讀取。另外,還有下面幾種方式也比較常用:
    從網絡中獲取:比如10年前十分流行Applet。
    根據一定的規則實時生成,比如設計模式中的動態代理模式,就是根據相應的類自動生成它的代理類。
    從非class文件中獲取,其實這與直接從class文件中獲取的方式本質上是一樣的,這些非class文件在jvm中運行之前會被轉換爲可被jvm所識別的字節碼文件。
對於加載的時機,各個虛擬機的做法並不一樣,但是有一個原則,就是當jvm“預期”到一個類將要被使用時,就會在使用它之前對這個類進行加載。比如說,在一段代碼中出現了一個類的名字,jvm在執行這段代碼之前並不能確定這個類是否會被使用到,於是,有些jvm會在執行前就加載這個類,而有些則在真正需要用的時候纔會去加載它,這取決於具體的jvm實現。我們常用的hotspot虛擬機是採用的後者,就是說當真正用到一個類的時候纔對它進行加載。
加載階段是類的生命週期中的第一個階段,加載階段之後,是連接階段。有一點需要注意,就是有時連接階段並不會等加載階段完全完成之後纔開始,而是交叉進行,可能一個類只加載了一部分之後,連接階段就已經開始了。但是這兩個階段總的開始時間和完成時間總是固定的:加載階段總是在連接階段之前開始,連接階段總是在加載階段完成之後完成。

連接


連接階段比較複雜,一般會跟加載階段和初始化階段交叉進行,這個階段的主要任務就是做一些加載後的驗證工作以及一些初始化前的準備工作,可以細分爲三個步驟:驗證、準備和解析(可選)。
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會將所有的類或接口名、字段名、方法名轉換爲具體的內存地址。
4、連接階段完成之後會根據使用的情況(直接引用還是被動引用)來選擇是否對類進行初始化。

初始化


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

    (a)通過new關鍵字實例化對象、讀取或設置類的靜態變量、調用類的靜態方法。
    (b)通過反射方式執行以上三種行爲。
    (c)初始化子類的時候,會觸發父類的初始化。
    (d)作爲程序入口直接運行時(也就是直接調用main方法)。
除了以上四種情況,其他使用類的方式叫做被動引用,而被動引用不會觸發類的初始化

被動引用的情況有:

    (1)引用父類的靜態字段,只會引起父類的初始化,而不會引起子類的初始化。
    (2)定義類數組,不會引起類的初始化。
    (3)引用類的常量,不會引起類的初始化。

請看主動引用的示例代碼:

import java.lang.reflect.Field;  
import java.lang.reflect.Method;  
 
class InitClass{  
    static {  
        System.out.println("初始化InitClass");  
    }  
    public static String a = null;  
    public static void method(){}  
}  
 
class SubInitClass extends InitClass{}  
 
public class Test1 {  
 
    /**  
     * 主動引用引起類的初始化的第四種情況就是運行Test1的main方法時  
     * 導致Test1初始化,這一點很好理解,就不特別演示了。  
     * 本代碼演示了前三種情況,以下代碼都會引起InitClass的初始化,  
     * 但由於初始化只會進行一次,運行時請將註解去掉,依次運行查看結果。  
     * @param args  
     * @throws Exception  
     */ 
    public static void main(String[] args) throws Exception{  
    //  主動引用引起類的初始化一: new對象、讀取或設置類的靜態變量、調用類的靜態方法。  
    //  new InitClass();  
    //  InitClass.a = "";  
    //  String a = InitClass.a;  
    //  InitClass.method();  
          
    //  主動引用引起類的初始化二:通過反射實例化對象、讀取或設置類的靜態變量、調用類的靜態方法。  
    //  Class cls = InitClass.class;  
    //  cls.newInstance();  
          
    //  Field f = cls.getDeclaredField("a");  
    //  f.get(null);  
    //  f.set(null, "s");  
      
    //  Method md = cls.getDeclaredMethod("method");  
    //  md.invoke(null, null);  
              
    //  主動引用引起類的初始化三:實例化子類,引起父類初始化。  
    //  new SubInitClass();  
 
    }  
} 
上面的程序演示了主動引用觸發類的初始化的四種情況。


類的初始化過程是這樣的:
1.在類的聲明裏查看有無靜態元素,如果有static element則首先執行其中語句,但注意static element只執行一次,在第二次創建類的對象的時候,就不會去執行static element的語句.
2.查看此類是否爲啓動運行類,若爲啓動運行類,則執行main()方法裏的語句對應語句
3.若不是啓動運行類,則按代碼的排版先後順序繼續執行非static element的變量賦值以及代碼塊.
4.最後執行構造方法,如果在被調用的構造方法裏面有this關鍵字(注意,如果你考慮要調用其他構造方法,則應該把this寫在最前面,不然會產生錯誤),則先調用相應構造方法主體,調用完之後再執行自己的剩下語句.

類初始化舉例如下:

public class FirstClass {
	FirstClass(int i){
		System.out.println("FirstClass("+i+")");
	}
	public void useMethod(int k){
		System.out.println("useMethod("+k+")");
	}
}
public class SecondClass{
	static FirstClass fc1=new FirstClass(1);
	public FirstClass fc3=new FirstClass(3);
	static{
		FirstClass fc2=new FirstClass(2);
	}
	public void aaa(){
		System.out.println("aaaaaaaaaaaaaaaaa");
	}
	SecondClass(){
		System.out.println("SecondClass()");
	}
	public FirstClass fc4=new FirstClass(4);
}
public class InitatalDemo {
	SecondClass sc1=new SecondClass();
	public void bbbb(){
		System.out.println("hello java world");
	}
	public static void main(String[] args) {
		System.out.println("Inside main()");
		SecondClass.fc1.useMethod(100);
		InitatalDemo idObj = new InitatalDemo();
	}
	static SecondClass sc2=new SecondClass();
}
//運行結果
FirstClass(1)
FirstClass(2)
FirstClass(3)
FirstClass(4)
SecondClass()
Inside main()
useMethod(100)
FirstClass(3)
FirstClass(4)
SecondClass()


被動引用的示例代碼:

class InitClass{  
    static {  
        System.out.println("初始化InitClass");  
    }  
    public static String a = null;  
    public final static String b = "b";  
    public static void method(){}  
}  
 
class SubInitClass extends InitClass{  
    static {  
        System.out.println("初始化SubInitClass");  
    }  
}  
 
public class Test4 {  
    public static void main(String[] args) throws Exception{  
    //  String a = SubInitClass.a;// 引用父類的靜態字段,只會引起父類初始化,而不會引起子類的初始化  
    //  String b = InitClass.b;// 使用類的常量不會引起類的初始化  
        SubInitClass[] sc = new SubInitClass[10];// 定義類數組不會引起類的初始化  
    }  
} 


需要說明的是:Java 運行環境爲了優化系統,提高程序的執行速度,在 JRE 運行的開始會將 Java 運行所需要的基本類採用預先加載( pre-loading )的方法全部加載(這裏說類加載指的是類的生命週期中加載、連接、初始化三個階段到內存當中,因爲這些單元在 Java 程序運行的過程當中經常要使用的,主要包括 JRE 的 rt.jar 文件裏面所有的 .class 文件 java.exe 虛擬機開始運行以後,它會找到安裝在機器上的 JRE 環境,然後把控制權交給 JRE , JRE 的類加載器會將 lib 目錄下的 rt.jar 基礎類別文件庫加載進內存,這些文件是 Java 程序執行所必須的,所以系統在開始就將這些文件加載,避免以後的多次 IO 操作,從而提高程序執行效率。
相對於預先加載,我們在程序中需要使用自己定義的類的時候就要使用依需求加載方法( load-on-demand ),就是在 Java 程序需要用到的時候再加載,以減少內存的消耗,因爲 Java 語言的設計初衷就是面向嵌入式領域的。 

使用

使用階段包括主動引用和被動引用,主動飲用會引起類的初始化,而被動引用不會引起類的初始化。當使用階段完成之後,java類就進入了卸載階段。

卸載

關於類的卸載,筆者在單例模式討論篇:單例模式與垃圾回收一文中有過描述,在類使用完之後,如果滿足下面的情況,類就會被卸載:
    (1)該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例。
    (2)加載該類的ClassLoader已經被回收。
    (3)該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
如果以上三個條件全部滿足,jvm就會在方法區垃圾回收的時候對類進行卸載,類的卸載過程其實就是在方法區中清空類信息,java類的整個生命週期就結束了。


二、java對象的生命週期

java垃圾回收


java堆是程序運行時分配給對象的生存空間,java堆越大,垃圾回收的的頻率就越低,java堆越小,垃圾回收的頻率就越高
     -Xms:初始堆大小
     -Xmx:最大堆大小
     -XX:NewSize=n:設置年輕代大小
     -XX:NewRatio=n:設置年輕代和年老代的比值。如:爲3,表示年輕代與年老代比值爲1:3,年輕代佔整個年輕代年老代和的1/4
     -XX:MaxPermSize=n:設置持久代大小


對象的生命週期


年輕代:所有新生成對象,存放在Eden區(頻繁的被GC)
年老代:經過N次垃圾回收仍然存活的對象,存放在Survivor區(GC頻率相對較低)
持久代:存放靜態文件,比如java類、方法等,垃圾回收對它沒有顯著的效果

總結


做java的朋友對於對象的生命週期可能都比較熟悉,對象基本上都是在jvm的堆區中創建,在創建對象之前,會觸發類加載(加載、連接、初始化),當類初始化完成後,根據類信息在堆區中實例化類對象,初始化非靜態變量、非靜態代碼以及默認構造方法,當對象使用完之後會在合適的時候被jvm垃圾收集器回收。對象的生命週期只是類的生命週期中使用階段的主動引用的一種情況(即實例化類對象)。而類的整個生命週期則要比對象的生命週期長的多。

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