天天看點

1. 垃圾收集簡介 - GC參考手冊1. 垃圾收集簡介

說明:

在本文中,

Garbage Collection

翻譯為 “

垃圾收集

”,

garbage collector

翻譯為 “

垃圾收集器

”;

一般認為, 垃圾回收 和 垃圾收集 是同義詞。

Minor GC

翻譯為: 小型GC; 而不是 次要GC

Major GC

翻譯為: 大型GC; 而不是 主要GC

原因在于,大部分情況下, 發生在年輕代的

Minor GC

次數會很多,翻譯為次要GC明顯不對。

Full GC

翻譯為: 完全GC; 為了清晰起見,一般直接譯為 Full GC,讀者明白即可; 其中大型GC和完全GC差不多, 這些術語出自官方的各種分析工具和垃圾收集日志。并不是很統一。

1. 垃圾收集簡介

顧名思義,垃圾收集(Garbage Collection)的意思就是 —— 找到垃圾并進行清理。但現有的垃圾收集實作卻恰恰相反: 垃圾收集器跟蹤所有正在使用的對象,并把其餘部分當做垃圾。記住這一點以後, 我們再深入講解記憶體自動回收的原理,探究 JVM 中垃圾收集的具體實作, 。

我們不摳細節, 先從基礎開始, 介紹垃圾收集的一般特征、核心概念以及實作算法。

免責聲明: 本文主要講解 Oracle Hotspot 和 OpenJDK 的行為。對于其他JVM, 如 jRockit 或者 IBM J9, 在某些方面可能會略有不同。

手動記憶體管理(Manual Memory Management)

當今的自動垃圾收集算法極為先進, 但我們先來看看什麼是手動記憶體管理。在那個時候, 如果要存儲共享資料, 必須顯式地進行 記憶體配置設定(allocate)和記憶體釋放(free)。如果忘記釋放, 則對應的那塊記憶體不能再次使用。記憶體一直被占着, 卻不再使用,這種情況就稱為記憶體洩漏(

memory leak

)。

以下是用C語言來手動管理記憶體的一個示例程式:

int send_request() {
    size_t n = read_size();
    int *elements = malloc(n * sizeof(int));

    if(read_elements(n, elements) < n) {
        // elements not freed!
        return -1;
    }

    // …

    free(elements)
    return 0;
}
           

可以看到,如果程式很長,或者結構比較複雜, 很可能就會忘記釋放記憶體。記憶體洩漏曾經是個非常普遍的問題, 而且隻能通過修複代碼來解決。是以,業界迫切希望有一種更好的辦法,來自動回收不再使用的記憶體,完全消除可能的人為錯誤。這種自動機制被稱為 垃圾收集(

Garbage Collection

,簡稱GC)。

智能指針(Smart Pointers)

第一代自動垃圾收集算法, 使用的是引用計數(reference counting)。針對每個對象, 隻需要記住被引用的次數, 當引用計數變為0時, 這個對象就可以被安全地回收(reclaimed)了。一個著名的示例是 C++ 的共享指針(shared pointers):

int send_request() {
    size_t n = read_size();
    shared_ptr<vector<int>> elements 
              = make_shared<vector<int>>();

    if(read_elements(n, elements) < n) {
        return -1;
    }

    return 0;
}
           

shared_ptr

被用來跟蹤引用的數量。作為參數傳遞時這個數字加1, 在離開作用域時這個數字減1。當引用計數變為0時,

shared_ptr

自動删除底層的 vector。需要向讀者指出的是,這種方式在實際程式設計中并不常見, 此處僅用于示範。

自動記憶體管理(Automated Memory Management)

上面的C++代碼中,我們要顯式地聲明什麼時候需要進行記憶體管理。但不能讓所有的對象都具備這種特征呢? 那樣就太友善了, 開發者不再耗費腦細胞, 去考慮要在何處進行記憶體清理。運作時環境會自動算出哪些記憶體不再使用,并将其釋放。換句話說, 自動進行收集垃圾。第一款垃圾收集器是1959年為Lisp語言開發的, 此後 Lisp 的垃圾收集技術也一直處于業界領先水準。

引用計數(Reference Counting)

剛剛示範的C++共享指針方式, 可以應用到所有對象。許多語言都采用這種方法, 包括 Perl、Python 和 PHP 等。下圖很好地展示了這種方式:

1. 垃圾收集簡介 - GC參考手冊1. 垃圾收集簡介

圖中綠色的雲(GC ROOTS) 表示程式正在使用的對象。從技術上講, 這些可能是目前正在執行的方法中的局部變量,或者是靜态變量一類。在某些程式設計語言中,可能叫法不太一樣,這裡不必摳名詞。

藍色的圓圈表示可以引用到的對象, 裡面的數字就是引用計數。然後, 灰色的圓圈是各個作用域都不再引用的對象。灰色的對象被認為是垃圾, 随時會被垃圾收集器清理。

看起來很棒, 是吧! 但這種方式有個大坑, 很容易被循環引用(

detached cycle

) 給搞死。任何作用域中都沒有引用指向這些對象,但由于循環引用, 導緻引用計數一直大于零。如下圖所示:

1. 垃圾收集簡介 - GC參考手冊1. 垃圾收集簡介

看到了嗎? 紅色的對象實際上屬于垃圾。但由于引用計數的局限, 是以存在記憶體洩漏。

當然也有一些辦法來應對這種情況, 例如 “弱引用”(‘weak’ references), 或者使用另外的算法來排查循環引用等。前面提到的 Perl、Python 和PHP 等語言, 都使用了某些方式來解決循環引用問題, 但本文不對其進行讨論。下面介紹JVM中使用的垃圾收集方法。

标記-清除(Mark and Sweep)

首先, JVM 明确定義了什麼是對象的可達性(reachability)。我們前面所說的綠色雲這種隻能算是模糊的定義, JVM 中有一類很明确很具體的對象, 稱為 垃圾收集根元素(

Garbage Collection Roots

),包括:

  • 局部變量(Local variables)
  • 活動線程(Active threads)
  • 靜态域(Static fields)
  • JNI引用(JNI references)
  • 其他對象(稍後介紹 …)

JVM使用标記-清除算法(Mark and Sweep algorithm), 來跟蹤所有的可達對象(即存活對象), 確定所有不可達對象(non-reachable objects)占用的記憶體都能被重用。其中包含兩步:

  • Marking

    (标記): 周遊所有的可達對象,并在本地記憶體(native)中分門别類記下。
  • Sweeping

    (清除): 這一步保證了,不可達對象所占用的記憶體, 在之後進行記憶體配置設定時可以重用。

JVM中包含了多種GC算法, 如Parallel Scavenge(并行清除), Parallel Mark+Copy(并行标記+複制) 以及 CMS, 他們在實作上略有不同, 但理論上都采用了以上兩個步驟。

标記清除算法最重要的優勢, 就是不再因為循環引用而導緻記憶體洩露:

1. 垃圾收集簡介 - GC參考手冊1. 垃圾收集簡介

而不好的地方在于, 垃圾收集過程中, 需要暫停應用程式的所有線程。假如不暫停,則對象間的引用關系會一直不停地發生變化, 那樣就沒法進行統計了。這種情況叫做 STW停頓(

Stop The World pause

, 全線暫停), 讓應用程式暫時停止,讓JVM進行記憶體清理工作。有很多原因會觸發 STW停頓, 其中垃圾收集是最主要的因素。

在本手冊中,我們将介紹JVM中垃圾收集的實作原理,以及如何高效地利用GC。

請繼續閱讀下一章: 2. Java中的垃圾收集 - GC參考手冊

原文連結: What Is Garbage Collection?

翻譯人員: 鐵錨 http://blog.csdn.net/renfufei

翻譯時間: 2015年10月26日