java垃圾回收

java垃圾回收,JAVA特有功能,垃圾收集意味著程式不再需要的對象是"無用信息",這些信息將被丟棄回收。

垃圾收集意義

在C++中,對象所占的記憶體在程式結束運行之前一直被占用,在明確釋放之前不能分配給其它對象;而在Java中,當沒有對象引用指向原先分配給某個對象的記憶體時,該記憶體便成為垃圾。JVM的一個系統級執行緒會自動釋放該記憶體塊。垃圾收集意味著程式不再需要的對象是"無用信息",這些信息將被丟棄。當一個對象不再被引用的時候,記憶體回收它占領的空間,以便空間被後來的新對象使用。事實上,除了釋放沒用的對象,垃圾收集也可以清除記憶體記錄碎片。由於創建對象和垃圾收集器釋放丟棄對象所占的記憶體空間,記憶體會出現碎片。碎片是分配給對象的記憶體塊之間的空閒記憶體洞。碎片整理將所占用的堆記憶體移到堆的一端,JVM將整理出的記憶體分配給新的對象。
垃圾收集能自動釋放記憶體空間,減輕編程的負擔。這使Java 虛擬機具有一些優點。首先,它能使編程效率提高。在沒有垃圾收集機制的時候,可能要花許多時間來解決一個難懂的存儲器問題。在用Java語言編程的時候,靠垃圾收集機制可大大縮短時間。其次是它保護程式的完整性, 垃圾收集是Java語言安全性策略的一個重要部份。
垃圾收集的一個潛在的缺點是它的開銷影響程式性能。Java虛擬機必須追蹤運行程式中有用的對象,而且最終釋放沒用的對象。這一個過程需要花費處理器的時間。其次垃圾收集算法的不完備性,早先採用的某些垃圾收集算法就不能保證100%收集到所有的廢棄記憶體。當然隨著垃圾收集算法的不斷改進以及軟硬體運行效率的不斷提升,這些問題都可以迎刃而解。
一般來說,Java開發人員可以不重視JVM中堆記憶體的分配和垃圾處理收集,但是,充分理解Java的這一特性可以讓我們更有效地利用資源。同時要注意finalize()方法是Java的預設機制,有時為確保對象資源的明確釋放,可以編寫自己的finalize方法。

算法分析

Java語言規範沒有明確地說明JVM使用哪種垃圾回收算法,但是任何一種垃圾收集算法一般要做2件基本的事情:(1)發現無用信息對象;(2)回收被無用對象占用的記憶體空間,使該空間可被程式再次使用。
大多數垃圾回收算法使用了根集(root set)這個概念;所謂根集就是正在執行的Java程式可以訪問的引用變數的集合(包括局部變數、參數、類變數),程式可以使用引用變數訪問對象的屬性和調用對象的方法。垃圾收集首選需要確定從根開始哪些是可達的和哪些是不可達的,從根集可達的對象都是活動對象,它們不能作為垃圾被回收,這也包括從根集間接可達的對象。而根集通過任意路徑不可達的對象符合垃圾收集的條件,應該被回收。下面介紹幾個常用的算法。
1、 引用計數法(Reference Counting Collector)
引用計數法是唯一沒有使用根集的垃圾回收的法,該算法使用引用計數器來區分存活對象和不再使用的對象。一般來說,堆中的每個對象對應一個引用計數器。當每一次創建一個對象並賦給一個變數時,引用計數器置為1。當對象被賦給任意變數時,引用計數器每次加1當對象出了作用域後(該對象丟棄不再使用),引用計數器減1,一旦引用計數器為0,對象就滿足了垃圾收集的條件。
基於引用計數器的垃圾收集器運行較快,不會長時間中斷程式執行,適宜地必須 實時運行的程式。但引用計數器增加了程式執行的開銷,因為每次對象賦給新的變數,計數器加1,而每次現有對象出了作用域生,計數器減1。
2、tracing算法(Tracing Collector)
tracing算法是為了解決引用計數法的問題而提出,它使用了根集的概念。基於tracing算法的垃圾收集器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,並用某種方式標記可達對象,例如對每個可達對象設定一個或多個位。在掃描識別過程中,基於tracing算法的垃圾收集也稱為標記和清除(mark-and-sweep)垃圾收集器.
3、compacting算法(Compacting Collector)
為了解決堆碎片問題,基於tracing的垃圾回收吸收了Compacting算法的思想,在清除的過程中,算法將所有的對象移到堆的一端,堆的另一端就變成了一個相鄰的空閒記憶體區,收集器會對它移動的所有對象的所有引用進行更新,使得這些引用在新的位置能識別原來的對象。在基於Compacting算法的收集器的實現中,一般增加句柄和句柄表。
4、copying算法(Coping Collector)
該算法的提出是為了克服句柄的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個對象 面和多個空閒面,程式從對象面為對象分配空間,當對象滿了,基於coping算法的垃圾 收集就從根集中掃描活動對象,並將每個活動對象複製到空閒面(使得活動對象所占的記憶體之間沒有空閒洞),這樣空閒面變成了對象面,原來的對象面變成了空閒面,程式會在新的對象面中分配記憶體。
一種典型的基於coping算法的垃圾回收是stop-and-copy算法,它將堆分成對象面和空閒區域面,在對象面與空閒區域面的切換過程中,程式暫停執行。
5、generation算法(Generational Collector)
stop-and-copy垃圾收集器的一個缺陷是收集器必須複製所有的活動對象,這增加了程式等待時間,這是coping算法低效的原因。在程式設計中有這樣的規律:多數對象存在的時間比較短,少數的存在時間比較長。因此,generation算法將堆分成兩個或多個,每個子堆作為對象的一代 (generation)。由於多數對象存在的時間比較短,隨著程式丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行後,上次運行存活下來的對象移到下一最高代的子堆中,由於老一代的子堆不會經常被回收,因而節省了時間。
6、adaptive算法(Adaptive Collector)
在特定的情況下,一些垃圾收集算法會優於其它算法。基於Adaptive算法的垃圾收集器就是監控當前堆的使用情況,並將選擇適當算法的垃圾收集器。

透視回收

1、命令行參數透視垃圾收集器的運行
使用System.gc()可以不管JVM使用的是哪一種垃圾回收的算法,都可以請求Java的垃圾回收。在命令行中有一個參數-verbosegc可以查看Java使用的堆記憶體的情況,它的格式如下:
java -verbosegc classfile
可以看個例子:
class TestGC
{
public static void main(String[] args)
{
new TestGC();
System.gc();
System.runFinalization();
}
}
在這個例子中,一個新的對象被創建,由於它沒有使用,所以該對象迅速地變為可達,程式編譯後,執行命令: java -verbosegc TestGC 後結果為:
[Full GC 168K->97K(1984K), 0.0253873SECS]
機器的環境為,Windows 2000 + JDK1.3.1,箭頭前後的數據168K和97K分別表示垃圾收集GC前後所有存活對象使用的記憶體容量,說明有168K-97K=71K的對象容量被回收,括弧內的數據1984K為堆記憶體的總容量,收集所需要的時間是0.0253873秒(這個時間在每次執行的時候會有所不同)。
2、finalize方法透視垃圾收集器的運行
在JVM垃圾收集器收集一個對象之前 ,一般要求程式調用適當的方法釋放資源,但在沒有明確釋放資源的情況下,Java提供了預設機制來終止化該對象心釋放資源,這個方法就是finalize()。它的原型為:
protected void finalize() throws Throwable
在finalize()方法返回之後,對象消失,垃圾收集開始執行。原型中的throws Throwable表示它可以拋出任何類型的異常。
之所以要使用finalize(),是由於有時需要採取與Java的普通方法不同的一種方法,通過分配記憶體來做一些具有C風格的事情。這主要可以通過"固有方法"來進行,它是從Java里調用非Java方法的一種方式。C和C++是目前唯一獲得固有方法支持的語言。但由於它們能調用通過其他語言編寫的子程式,所以能夠有效地調用任何東西。在非Java代碼內部,也許能調用C的malloc()系列函式,用它分配存儲空間。而且除非調用了 free(),否則存儲空間不會得到釋放,從而造成記憶體"漏洞"的出現。當然,free()是一個C和C++函式,所以我們需要在finalize()內部的一個固有方法中調用它。也就是說我們不能過多地使用finalize(),它並不是進行普通清除工作的理想場所。
在普通的清除工作中,為清除一個對象,那個對象的用戶必須在希望進行清除的地點調用一個清除方法。這與C++"破壞器"的概念稍有牴觸。在C++中,所有對象都會破壞(清除)。或者換句話說,所有對象都"應該"破壞。若將C++對象創建成一個本地對象,比如在堆疊中創建(在Java中是不可能的),那么清除或破壞工作就會在"結束花括弧"所代表的、創建這個對象的作用域的末尾進行。若對象是用new創建的(類似於Java),那么當程式設計師調用C++的 delete命令時(Java沒有這個命令),就會調用相應的破壞器。若程式設計師忘記了,那么永遠不會調用破壞器,我們最終得到的將是一個記憶體"漏洞",另外還包括對象的其他部分永遠不會得到清除。
相反,Java不允許我們創建本地(局部)對象--無論如何都要使用new。但在Java中,沒有"delete"命令來釋放對象,因為垃圾收集器會幫助我們自動釋放存儲空間。所以如果站在比較簡化的立場,我們可以說正是由於存在垃圾收集機制,所以Java沒有破壞器。然而,隨著以後學習的深入,就會知道垃圾收集器的存在並不能完全消除對破壞器的需要,或者說不能消除對破壞器代表的那種機制的需要(而且絕對不能直接調用finalize(),所以應儘量避免用它)。若希望執行除釋放存儲空間之外的其他某種形式的清除工作,仍然必須調用Java中的一個方法。它等價於C++的破壞器,只是沒後者方便。
下面這個例子向大家展示了垃圾收集所經歷的過程,並對前面的陳述進行了總結。
class Chair {
static boolean gcrun = false;
static boolean f = false;
static int created = 0;
static int finalized = 0;
int i;
Chair() {
i = ++created;
if(created == 47)
System.out.println("Created 47");
}
protected void finalize() {
if(!gcrun) {
gcrun = true;
System.out.println("Beginning to finalize after " + created + " Chairs have been created");
}
if(i == 47) {
System.out.println("Finalizing Chair #47, " +"Setting flag to stop Chair creation");
f = true;
}
finalized++;
if(finalized >= created)
System.out.println("All " + finalized + " finalized");
}
}
public class Garbage {
public static void main(String[] args) {
if(args.length == 0) {
System.err.println("Usage: \n" + "java Garbage before\n or:\n" + "java Garbage after");
return;
}
while(!Chair.f) {
new Chair();
new String("To take up space");
}
System.out.println("After all Chairs have been created:\n" + "total created = " + Chair.created +
", total finalized = " + Chair.finalized);
if(args.equals("before")) {
System.out.println("gc():");
System.gc();
System.out.println("runFinalization():");
System.runFinalization();
}
System.out.println("bye!");
if(args.equals("after"))
System.runFinalizersOnExit(true);
}
}
上面這個程式創建了許多Chair對象,而且在垃圾收集器開始運行後的某些時候,程式會停止創建Chair。由於垃圾收集器可能在任何時間運行,所以我們不能準確知道它在何時啟動。因此,程式用一個名為gcrun的標記來指出垃圾收集器是否已經開始運行。利用第二個標記f,Chair可告訴 main()它應停止對象的生成。這兩個標記都是在finalize()內部設定的,它調用於垃圾收集期間。另兩個static變數--created以及finalized--分別用於跟蹤已創建的對象數量以及垃圾收集器已進行完收尾工作的對象數量。最後,每個Chair都有它自己的(非 static)int i,所以能跟蹤了解它具體的編號是多少。編號為47的Chair進行完收尾工作後,標記會設為true,最終結束Chair對象的創建過程。

關於補充

經過上述的說明,可以發現垃圾回收有以下的幾個特點:
(1)垃圾收集發生的不可預知性:由於實現了不同的垃圾收集算法和採用了不同的收集機制,所以它有可能是定時發生,有可能是當出現系統空閒CPU資源時發生,也有可能是和原始的垃圾收集一樣,等到記憶體消耗出現極限時發生,這與垃圾收集器的選擇和具體的設定都有關係。
(2)垃圾收集的精確性:主要包括2 個方面:(a)垃圾收集器能夠精確標記活著的對象;(b)垃圾收集器能夠精確地定位對象之間的引用關係。前者是完全地回收所有廢棄對象的前提,否則就可能造成記憶體泄漏。而後者則是實現歸併和複製等算法的必要條件。所有不可達對象都能夠可靠地得到回收,所有對象都能夠重新分配,允許對象的複製和對象記憶體的縮並,這樣就有效地防止記憶體的支離破碎。
(3)現在有許多種不同的垃圾收集器,每種有其算法且其表現各異,既有當垃圾收集開始時就停止應用程式的運行,又有當垃圾收集開始時也允許應用程式的執行緒運行,還有在同一時間垃圾收集多執行緒運行。
(4)垃圾收集的實現和具體的JVM 以及JVM的記憶體模型有非常緊密的關係。不同的JVM 可能採用不同的垃圾收集,而JVM 的記憶體模型決定著該JVM可以採用哪些類型垃圾收集。現在,HotSpot 系列JVM中的記憶體系統都採用先進的面向對象的框架設計,這使得該系列JVM都可以採用最先進的垃圾收集。
(5)隨著技術的發展,現代垃圾收集技術提供許多可選的垃圾收集器,而且在配置每種收集器的時候又可以設定不同的參數,這就使得根據不同的套用環境獲得最優的套用性能成為可能。

注意

(1)不要試圖去假定垃圾收集發生的時間,這一切都是未知的。比如,方法中的一個臨時對象在方法調用完畢後就變成了無用對象,這個時候它的記憶體就可以被釋放。
(2)Java中提供了一些和垃圾收集打交道的類,而且提供了一種強行執行垃圾收集的方法--調用System.gc(),但這同樣是個不確定的方法。Java 中並不保證每次調用該方法就一定能夠啟動垃圾收集,它只不過會向JVM發出這樣一個申請,到底是否真正執行垃圾收集,一切都是個未知數。
(3)挑選適合自己的垃圾收集器。一般來說,如果系統沒有特殊和苛刻的性能要求,可以採用JVM的預設選項。否則可以考慮使用有針對性的垃圾收集器,比如增量收集器就比較適合實時性要求較高的系統之中。系統具有較高的配置,有比較多的閒置資源,可以考慮使用並行標記/清除收集器。
(4)關鍵的也是難把握的問題是記憶體泄漏。良好的編程習慣和嚴謹的編程態度永遠是最重要的,不要讓自己的一個小錯誤導致記憶體出現大漏洞。
(5)儘早釋放無用對象的引用。大多數程式設計師在使用臨時變數的時候,都是讓引用變數在退出活動域(scope)後,自動設定為null,暗示垃圾收集器來收集該對象,還必須注意該引用的對象是否被監聽,如果有,則要去掉監聽器,然後再賦空值。

相關詞條

相關搜尋

熱門詞條

聯絡我們