java垃圾回收機制整理

VirLB發表於2019-07-01

一、垃圾回收器和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技術就是採用了類似方法,程式碼每次執行都會做一些優化,所以執行次數越多,它的速度就越快。

 

相關文章