在JVM運行空間中,對象的整個生命周期大致可以分為7個階段:創建階段 (Creation)、應用階段(Using)、不可視階段(Invisible)、不可到達階段 (Unreachable)、可收集階段(Collected)、終結階段(Finalized)與釋放 階段(Free)。上面的這7個階段,構成了 JVM中對象的完整的生命周期。下面 分別介紹對象在處於這7個階段時的不同情形。
創建階段
在對象創建階段,系統要通過下面的步驟,完成對象的創建過程:
(1)為對象分配存儲空間。
(2)開始構造對象。
(3)遞歸調用其超類的構造方法。
(4)進行對象實例初始化與變量初始化。
(5)執行構造方法體。
上面的5個步驟中的第3步就是指遞歸地調用該類所擴展的所有父類的構造方 法,一個Java類(除Object類外)至少有一個父類(Object),這個規則既是強 制的,也是隱式的。你可能已經注意到在創建一個Java類的時候,並沒有顯式地 聲明擴展(extends)一個Object父類。實際上,在 Java程序設計中,任何一個 Java類都直接或間接的是Object類的子類。例如下面的代碼:
public class A {
…
}
這個聲明等同於下面的聲明:
public class A extends java.lang.Object {
…
}
上面講解了對象處於創建階段時,系統所做的一些處理工作,其中有些過程 與應用的性能密切相關,因此在創建對象時,我們應該遵循一些基本的規則,以 提高應用的性能。
下面是在創建對象時的幾個關鍵應用規則:
(1)避免在循環體中創建對象,即使該對象占用內存空間不大。
(2)盡量及時使對象符合垃圾回收標准。
(3)不要采用過深的繼承層次。
(4)訪問本地變量優於訪問類中的變量。
關於規則(1)避免在循環體中創建對象,即使該對象占用內存空間不大,需 要提示一下,這種情況在我們的實際應用中經常遇到,而且我們很容易犯類似的 錯誤,例如下面的代碼:
… …
for (int i = 0; i < 10000; ++i) {
Object obj = new Object();
System.out.println("obj= "+ obj);
}
… …
上面代碼的書寫方式相信對你來說不會陌生,也許在以前的應用開發中你也 這樣做過,尤其是在枚舉一個Vector對象中的對象元素的操作中經常會這樣書寫 ,但這卻違反了上述規則(1),因為這樣會浪費較大的內存空間,正確的方法 如下所示:
… …
Object obj = null;
for (int i = 0; i < 10000; ++i) {
obj = new Object();
System.out.println("obj= "+ obj);
}
… …
采用上面的第二種編寫方式,僅在內存中保存一份對該對象的引用,而不像 上面的第一種編寫方式中代碼會在內存中產生大量的對象應用,浪費大量的內存 空間,而且增大了系統做垃圾回收的負荷。因此在循環體中聲明創建對象的編寫 方式應該盡量避免。
另外,不要對一個對象進行多次初始化,這同樣會帶來較大的內存開銷,降 低系統性能,如:
public class A {
private Hashtable table = new Hashtable ();
public A() {
// 將Hashtable對象table初始化了兩次
table = new Hashtable();
}
}
正確的方式為:
public class B {
private Hashtable table = new Hashtable ();
public B() {
}
}
不要小看這個差別,它卻使應用軟件的性能相差甚遠,如圖2-5所示。
圖2-5 初始化對象多次所帶來的性能差別
看來在程序設計中也應該遵從“勿以惡小而為之”的古訓,否則我們開發出 來的應用也是低效的應用,有時應用軟件中的一個極小的失誤,就會大幅度地降 低整個系統的性能。因此,我們在日常的應用開發中,應該認真對待每一行代碼 ,采用最優化的編寫方式,不要忽視細節,不要忽視潛在的問題。
應用階段
當對象的創建階段結束之後,該對象通常就會進入對象的應用階段。這個階 段是對象得以表現自身能力的階段。也就是說對象的應用階段是對象整個生命周 期中證明自身“存在價值”的時期。在對象的應用階段,對象具備下列特征:
◆系統至少維護著對象的一個強引用(Strong Reference);
◆所有對該對象的引用全部是強引用(除非我們顯式地使用了:軟引用 (Soft Reference)、弱引用(Weak Reference)或虛引用(Phantom Reference))。
上面提到了幾種不同的引用類型。可能一些讀者對這幾種引用的概念還不是 很清楚,下面分別對之加以介紹。在講解這幾種不同類型的引用之前,我們必須 先了解一下Java中對象引用的結構層次。
Java對象引用的結構層次示意如圖2-6所示。
圖2-6 對象引用的結構層次示意
由圖2-6我們不難看出,上面所提到的幾種引用的層次關系,其中強引用處於 頂端,而虛引用則處於底端。下面分別予以介紹。
1.強引用
強引用(Strong Reference)是指JVM內存管理器從根引用集合(Root Set) 出發遍尋堆中所有到達對象的路徑。當到達某對象的任意路徑都不含有引用對象 時,對這個對象的引用就被稱為強引用。
2.軟引用
軟引用(Soft Reference)的主要特點是具有較強的引用功能。只有當內存 不夠的時候,才回收這類內存,因此在內存足夠的時候,它們通常不被回收。另 外,這些引用對象還能保證在Java拋出OutOfMemory 異常之前,被設置為null。 它可以用於實現一些常用資源的緩存,實現Cache的功能,保證最大限度的使用 內存而不引起OutOfMemory。再者,軟可到達對象的所有軟引用都要保證在虛擬 機拋出OutOfMemoryError
之前已經被清除。否則,清除軟引用的時間或者清除不同對象的一組此類引 用的順序將不受任何約束。然而,虛擬機實現不鼓勵清除最近訪問或使用過的軟 引用。下面是軟引用的實現代碼:
… …
import java.lang.ref.SoftReference;
…
A a = new A();
…
// 使用 a
…
// 使用完了a,將它設置為soft 引用類型,並且釋放強引用;
SoftReference sr = new SoftReference(a);
a = null;
…
// 下次使用時
if (sr!=null) {
a = sr.get();
}
else{
// GC由於內存資源不足,可能系統已回收了a的軟引用,
// 因此需要重新裝載。
a = new A();
sr=new SoftReference(a);
}
… …
軟引用技術的引進,使Java應用可以更好地管理內存,穩定系統,防止系統 內存溢出,避免系統崩潰(crash)。因此在處理一些占用內存較大而且聲明周 期較長,但使用並不頻繁的對象時應盡量應用該技術。正像上面的代碼一樣,我 們可以在對象被回收之後重新創建(這裡是指那些沒有保留運行過程中狀態的對 象),提高應用對內存的使用效率,提高系統穩定性。但事物總是帶有兩面性的 ,有利亦有弊。在某些時候對軟引用的使用會降低應用的運行效率與性能,例如 :應用軟引用的對象的初始化過程較為耗時,或者對象的狀態在程序的運行過程 中發生了變化,都會給重新創建對象與初始化對象帶來不同程度的麻煩,有些時 候我們要權衡利弊擇時應用。
3.弱引用
弱引用(Weak Reference)對象與Soft引用對象的最大不同就在於:GC在進 行回收時,需要通過算法檢查是否回收Soft引用對象,而對於Weak引用對象, GC總是進行回收。因此Weak引用對象會更容易、更快被GC回收。雖然,GC在運行 時一定回收Weak引用對象,但是復雜關系的Weak對象群常常需要好幾次GC的運行 才能完成。Weak引用對象常常用於Map數據結構中,引用占用內存空間較大的對 象,一旦該對象的強引用為null時,對這個對象引用就不存在了,GC能夠快速地 回收該對象空間。與軟引用類似我們也可以給出相應的應用代碼:
… …
import java.lang.ref.WeakReference;
…
A a = new A();
…
// 使用 a
…
// 使用完了a,將它設置為weak 引用類型,並且釋放強引用;
WeakReference wr = new WeakReference (a);
a = null;
…
// 下次使用時
if (wr!=null) {
a = wr.get();
}
else{
a = new A();
wr = new WeakReference (a);
}
… …
弱引用技術主要適用於實現無法防止其鍵(或值)被回收的規范化映射。另 外,弱引用分為“短弱引用(Short Week Reference)”和“長弱引用(Long Week Reference)”,其區別是長弱引用在對象的Finalize方法被GC調用後依然 追蹤對象。基於安全考慮,不推薦使用長弱引用。因此建議使用下面的方式創建 對象的弱引用。
… …
WeakReference wr = new WeakReference(obj);
或
WeakReference wr = new WeakReference(obj, false);
… …
4.虛引用
虛引用(Phantom Reference)的用途較少,主要用於輔助finalize函數的使 用。Phantom對象指一些執行完了finalize函數,並且為不可達對象,但是還沒 有被GC回收的對象。這種對象可以輔助finalize進行一些後期的回收工作,我們 通過覆蓋Reference的clear()方法,增強資源回收機制的靈活性。虛引用主要適 用於以某種比 java 終結機制更靈活的方式調度 pre-mortem 清除操作。
&注意 在實際程序設計中一般很少使用弱引用與虛引用,使用軟引用 的情況較多,這是因為軟引用可以加速JVM對垃圾內存的回收速度,可以維護系 統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。
不可視階段
在一個對象經歷了應用階段之後,那麼該對象便處於不可視階段,說明我們 在其他區域的代碼中已經不可以再引用它,其強引用已經消失,例如,本地變量 超出了其可視范圍,如下所示。
… …
public void process () {
try {
Object obj = new Object();
obj.doSomething();
} catch (Exception e) {
e.printStackTrace();
}
while (isLoop) { // ... loops forever
// 這個區域對於obj對象來說已經是不可視的了
// 因此下面的代碼在編譯時會引發錯誤
obj.doSomething();
}
}
… …
如果一個對象已使用完,而且在其可視區域不再使用,此時應該主動將其設 置為空(null)。可以在上面的代碼行obj.doSomething();下添加代碼行obj = null;,這樣一行代碼強制將obj對象置為空值。這樣做的意義是,可以幫助JVM 及時地發現這個垃圾對象,並且可以及時地回收該對象所占用的系統資源。
不可到達階段
處於不可到達階段的對象,在虛擬機所管理的對象引用根集合中再也找不到 直接或間接的強引用,這些對象通常是指所有線程棧中的臨時變量,所有已裝載 的類的靜態變量或者對本地代碼接口(JNI)的引用。這些對象都是要被垃圾回 收器回收的預備對象,但此時該對象並不能被垃圾回收器直接回收。其實所有垃 圾回收算法所面臨的問題是相同的——找出由分配器分配的,但是用戶程序不可 到達的內存塊。
可收集階段、終結階段與釋放階段
對象生命周期的最後一個階段是可收集階段、終結階段與釋放階段。當對象 處於這個階段的時候,可能處於下面三種情況:
(1)垃圾回收器發現該對象已經不可到達。
(2)finalize方法已經被執行。
(3)對象空間已被重用。
當對象處於上面的三種情況時,該對象就處於可收集階段、終結階段與釋放 階段了。虛擬機就可以直接將該對象回收了。