java垃圾回收機制整理


一、垃圾回收器和finalize() 

 java垃圾回收器只負責回收無用對象占據的內存資源。但是如果你的對象不是通過 new 創建的(所有的new 對象都往堆中開辟資源,在一個地方,方便清理/管理資源),它會不知道該如果釋放該對象的這塊特殊內存。為了應對這個情況,Object自帶一個finalize()方法。

  finalize()這方法的原理是:一旦垃圾回收器准備釋放該對象占用的存儲空間,將會先調用其繼承/重寫的fialize(),並且調用方法后不是立即執行回收,而是在下一次(JVM覺得需要更大內存的時候)回收動作發生時,才會真正回收對象占用的內存。所以一般自己重寫fialize()方法,是在回收的最后時刻做一些重要的清理工作。

java垃圾回收幾個特點:

1、對象可能不被垃圾回收

   你創建的對象做了某個功能,比如顯示在電腦的屏幕上。那么除非你特別處理從屏幕上擦除,它永遠不可能得到清理。所以如果在finalize()方法中做擦除屏幕的處理,當垃圾回收時,finalize()被調用,屏幕圖像清除。請注意:垃圾回收器只有在JVM覺得需要更大內存的時候才會運行(雖然開銷小,但是一直運行還是有開銷的),所以大部分回收動作是發生在瀕臨存儲空間用完的那一刻,逼得JVM去運行垃圾回收器。如果程序執行結束 (或者中斷運行),那些資源也會全部還給操作系統。

2、垃圾回收並不等於析構

  這個是C的概念,因為java和C的牽扯太深,所以經常拿來對比。簡單說C有一個東西叫析構函數,在銷毀對象前必須執行這個析構函數。這里的垃圾回收並不代表析構。finalize()就是類似功能但是不等於。

3、垃圾回收只與內存有關

  這里就要講到finalize()的真正用途。該方法內部執行的操作也應該和內存及其回收有關,所以fialize()方法不是通用的方法。你可能會想到,當對象包含成員對象屬性的時候,finalize()是否應該明確要清除那些對象呢?不正確。應該這樣理解:無論對象如果創建,垃圾回收器都會負責釋放對象占用的所有內存。所以finalize()一般是來處理通過創建對象以外的方式為對象分配存儲空間。說起來有些繞口,但是舉個例子就知道了。

  java跟蹤源碼的時候經常遇到關鍵字native修飾的方法,這些方法也叫“本地方法“。在使用這些本地方法的時候,內部調用的是非java代碼的方式(不是C就是C++)。.這些非代碼中,也許會用到C的malloc()函數系列來分配存儲空間。這樣除非調用C的free()方法,否則存儲空間將不會釋放。所以可以在finalize()使用native方式調用free。

  以上,就是建議盡量少重寫finalize()的道理。

二、垃圾回收條件

  既然fialize()使用場景這么生僻,那就不要指望頻繁使用fialize()。你必須創建其他的清理方法,來自己根據業務清理。但是fialize()有個特點是:程序調用它,是該對象“終結條件“的驗證。也就是被標記了,該對象已死,可以回收了。例如:某個對象代表打開的一個文件,在對象被回收前程序員應該關閉文件。只要對象存在沒有被適當清理的部分,程序就存在隱晦的缺陷。fialize()可以用來最終發現這種情況。

 

 
         

// Using finalize() to detect an object that
// hasn't been properly cleaned up.

class Book {
  boolean checkedOut = false; Book(boolean checkOut) { checkedOut = checkOut; } void checkIn() { checkedOut = false; } protected void finalize() { if(checkedOut) System.out.println("Error: checked out"); // Normally, you'll also do this: // super.finalize(); // Call the base-class version  } } public class TerminationCondition { public static void main(String[] args) { Book novel = new Book(true); // Proper cleanup:  novel.checkIn(); // Drop the reference, forget to clean up: new Book(true); // Force garbage collection & finalization:  System.gc(); } }/* Output: Error: checked out *///:~

 

這個例子的終結條件是:所有Book對象在被當做垃圾回收前都應該checkIn。但是在main里面,第二本書沒有checkIn,這個時候通過finalize(),就能明確知道,有的對象沒有在銷毀前處理干凈了。另外,代碼中還使用了System.gc();這是強制喚起垃圾回收機器,來觸發BOOK的finalize();當然,如果不這樣強制喚起也行,當程序運行到被分配了大量內存的時候(可以大量反復創建BOOK),逼得垃圾回收器會自動觸發。如果BOOK有繼承某個父類,要觸發該父類的finalize(),可以使用super.finalize();調用。

三、垃圾回收器如何工作

  一般印象里面,在堆內分配新資源會比較慢,畢竟比不了堆棧快。但是其實JVM在這方面是做了大量的優化,其中垃圾回收器對於提高對象的創建速度,具有明顯效果。即使用垃圾回收器釋放存儲空間有利於未使用存儲空間的分配。通俗點就是說,垃圾回收器回收的內存越多,創建對象理論上會更快(還是有臨界的,一般認為媲美其他語言在堆棧中創建對象,比如C)。C的堆就好像一個院子,里面的每個對象各管各的存儲空間。一段時間以后某個對象被銷毀了。它的空間必須被重新使用。在某些JVM中,堆就像一個傳送帶,分配一個新的對象,它就往前移動一格。這個意味着空間分配會非常快(尋址快)。java的尋址指針只需要簡單移動到尚未分配的區域就行,這樣效率比得上C在堆棧上分配的速度。其中,記錄對象空間地址“下標“方面,還是有部分開銷的,但是比C需要查找堆的開銷,小得多。其實,java中的堆未必完全是像傳送帶,因為這會造成頻繁的內存頁面調度(內存是分頁的,翻頁時是要移出硬盤,放在虛擬內存上)。頁面調度會顯著影響性能,最終,在創建足夠的對象后,內存(大量虛擬內存充斥)資源耗盡。

  這里就輪到垃圾回收器登場了,當垃圾回收器工作的時候,一邊回收空間,一邊使堆中的對象排列緊湊。這樣“堆指針”就可以很容易移動到更靠近傳送帶的開始處(java堆分配空間是先進),也就盡量避免了頁面錯誤。垃圾回收期會對對象重新排列,實現高速的、有無限空間(?)可以分配的堆模型。

下面是垃圾回收(不止java)常用的三種設計方式:

1、引用計數

  每個對象含有一個應用計數。當有引用連接到對象的時候+1,引用離開作用域或者賦值null的時候-1。好了,那么當發現某個對象的引用是0的時候,就釋放它占用的空間(這里會出現一變為0就釋放空間)。這里就存在缺陷,如果對象循環引用,即A引用B,B引用A,就出現“對象可以被回收,但是引用計數不是0”的情況。對於垃圾回收器來說,定位這種互相引用的對象組開銷極大。另外,管理引用計數的開銷不大,但是這個會在整個程序生命周期內持續發生。引用計數的方式一般用來表述垃圾收集,但沒有應用於任何一種JVM中。

2、stop-and-copy

  這種方式是先暫停程序(不是后台運行,而是停止程序,執行垃圾回收),然后將所以存活的對象從當前的堆中復制到另外一個堆,剩下的都是垃圾。當對象被復制到新的家(堆)時,會把這些對象一個挨着一個,所以新堆保持緊湊排列。這個就是前面說的JVM虛擬機的垃圾回收期為什么能做到使對象緊湊排列了。復制過程會產生新的開銷,以及所有指向就舊對象的引用都要指向新的地址。這里可能出現來自非堆的引用(不是new出來的對象),這些會在遍歷舊堆的引用的時候被找出來,重新指向新堆。

  這種回收方式,效率低。首先,是因為要有兩個堆,然后對象要在兩個堆中復制轉移,所以實際上維護的空間比理論上大一倍。例如,某些JVM的做法是,在堆里面分配幾個大的內存塊,復制的操作在這里個內存塊中進行。其次,當程序運行趨於穩定以后,產生的垃圾比較少,甚至可能沒有垃圾。這個時候來回復制就很浪費了。為了避免浪費,JVM會進行檢查,要是沒有新垃圾產生,就自動轉換成下一種模式。

3、mark-and-sweep

  Sun公司早期版本的虛擬機使用的就是這個。這種方式的思路是從堆和靜態存儲區出發,遍歷所有的引用,然后就能找到所有存活對象。每當找到一個存活對象,就給該對象一個標記,記上一筆。所有標記都做完以后,清理開始。在清理的時候,沒有標記的對象將被釋放,不會發生復制動作。這個時候堆中就有點像C的樣子,所以如果要讓剩下的對象內存連續,就需要重新整理剩下的對象。

  

小結:

  Sun的文獻把垃圾回收看做是低優先級的后台進程,指的是因為stop-and-copy:畢竟要暫停程序。但是在早期版本中,JVM使用的是mark-and-sweep。現在這兩種回收方式通過JVM進行監視,如果有所對象都很穩定,垃圾回收器效率降低的話,就切換到mark-and-sweep。同樣的,如果mark-and-sweep的效果不好,堆中出現了很多垃圾碎片(無引用對象),就會切換到stop-and-copy。在stop-and-copy使用的時候,因為內存分配以較大的“塊”為單位,如果對象較大,它會占用整個塊。在stop-and-copy運行的到停止程序運行的操作前,會把所有存活的對象復制到新的塊(堆)中,這個時候舊的塊就會被廢棄,垃圾回收器就可以向廢棄的塊復制新對象進去,靈活利用資源。每個塊都有相應的代數(count)來記錄它是否還存活。如果塊在某處被引用,count會增加。垃圾回收器將對上次回收動作之后新分配的塊進行整理。這個対短命的對象很有幫助。垃圾回收期會定期進行完整的清理動作----大型對象仍然不會被復制,內含小型對象的那些塊還是會被復制並且整理。

  JVM有許多的附加技術用以提升速度。比如JIT(Just-In-Time)編譯器的技術。這個會把程序全部或部分翻譯成本地機器碼,程序運行速度因此快上很多。當需要裝載某個類的時候(創建該類的第一個對象,后續創建不會再次裝載),編譯器先找到對應的class文件,然后把字節碼內容裝入內存中。這個時候,有兩種方式可以選。其一是讓JIT編譯所有代碼轉換成機器碼。但是因為裝載的發生不可控制,是零散在整個程序的生命周期內的,累加起來就需要花費很多時間,並且會增加可執行代碼的長度(字節碼要比JIT編譯后展開的機器碼小很多),這個可能導致內存頁面調度,從而降低程序的速度。其二是惰性評估(lazy evaluatio),意思是JIT只有在必要的時候才編譯代碼,這樣從不會被執行的代碼(import 進來,但是沒有使用)就壓根不會被JIT編譯。JDK中的Java HotSpot技術就是采用了類似方法,代碼每次執行都會做一些優化,所以執行次數越多,它的速度就越快。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM