深入理解Java虛擬機 ch3 垃圾回收器和內存分配策略 讀書筆記

part2 自動內存管理機制

  本章接着上一章的內容繼續講。本章的內容分兩塊:垃圾收集器內存分配和回收策略,也就是內存的分配和回收。

章三 垃圾收集器和內存分配策略

  首先談內存分配。上一章節提到,JVM運行時內存分爲5個區域,其中程序計數器,虛擬機棧和本地方法棧這三個區域是線程私有的,隨線程創建而分配,線程死亡而收回,無需特別管理。而方法區和Java堆中的內存分配和回收則較爲複雜,尤其是Java堆,是本章研究的重點。

  下面談垃圾收集器。顧名思義,垃圾收集器負責垃圾回收。那麼,什麼是垃圾,什麼時候回收垃圾,怎樣回收垃圾?這三個問題就是有關垃圾回收器設計的最核心問題。隨後,還會講述一些重要的垃圾回收器。

一 垃圾回收

1.1 什麼是垃圾

  前面說到,垃圾回收器是針對Java堆和方法區這兩塊內存區域進行回收,那麼垃圾也就是這兩塊區域的垃圾。需要回收的垃圾主要分三種:Java堆中“已死”的對象實例,方法區中的廢棄常量無用的類

  下面的重點是:怎麼判斷一個對象實例“已死”,常量已被廢棄,類無用了?

  考慮到判斷對象實例的生死是這裏的重點,也是垃圾回收器回收最頻繁高效的區域,且廢棄常量的判定與其基本一致。這裏先講對無用的類的判定。對類無用的判定比較複雜,需要同時滿足下面三個條件:

  • 該類的所有實例已經被回收(Java堆中不存在該類實例)
  • 加載該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過放射訪問該類的方法

  對於滿足上述3個條件的無用類,JVM可以進行回收,根據設置而定。需要注意的是,對於大量使用反射、動態代理和CGLib等ByteCode框架、動態生成JSP及OSCi這類頻繁自定義ClassLoader的場景需要設置類卸裝功能,防止方法區(HotSpot中的永生代)溢出。一般情況下,不用擔心溢出問題。

  下面談這裏的重點,Java堆中的對象生死判定。這裏涉及到兩個算法:引用計數算法可達性分析算法

  引用計數算法很容易理解,具體實施如下:對每個對象添加一個引用計數器,每當有一個地方引用它時,計數器值加1;每當有一個地方不在引用它時,計數器值減1;計數器值爲0的對象則被判定爲垃圾,可以被回收。該算法簡單有效,Python的默認判定算法就是使用引用計數法的。引用計數法的優點很明顯:簡單高效,算法設計簡單,對於計數器爲0的對象可以立即清除。但該算法有個問題:循環計數。下面給出循環引用的示例代碼:

class Reference {
    public Reference ref = null;
}
class MainTest{

    public static void main(String[] args) {
        Reference ref1 = new Reference();
        Reference ref2 = new Reference(); //兩個對象的計數器分別加1

        ref1.ref = ref2;
        ref2.ref = ref1;    //兩個對象的計數器再分別加1,循環引用

        ref1 = null;
        ref2 = null;      //這裏,ref1和ref2應該被回收,但是兩個對象的計數器值爲1,
    }
}

  下面談可達性分析算法,JVM中就是通過該算法來判定對象是否存活的。以GC Roots對象爲起點,從這些節點想下搜索,得到整個引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,即GC Roots到這個對象不可達時,認爲該對象不可用,可以被回收。可以作爲GC Roots的對象包括以下:

  • 虛擬機棧(棧幀中本地變量表)中引用的對象
  • 本地方法棧中Native方法引用的對象
  • 方法區中類靜態屬性和常量引用的對象

  最後需要說明的時,對於由可達性分析算法中分析爲不可達的對象,在死亡前,會經歷至少兩次標記。且可以在finalize()函數中實現自救,但強烈不建議這樣去做。這裏也說明了,可達性分析算法的時效性不如引用計數法,該法引起的GC停頓也是後面要分析的重點。

1.2 怎麼回收:垃圾收集算法

  考慮到垃圾收集的具體操作十分繁瑣,這裏僅闡述主流算法的思想。涉及的算法包括標記-清除算法,複製算法和標記-整理算法。

  標記-清除算法思路很簡單:首先標記處所有需要會受到額對象,在標記完成後統一回收所有被標記的對象。本算法是最基礎的收集算法,其他算法都是在其不足之處進行改進。不足之處:一是效率問題,標記和清除兩國過程的效率都不高:二是空間問題,標記清除後會產生大量的空間碎片。

  複製算法是針對標記-清除算法產生大量空間碎片這個問題來進行設計的。基本設計思路如下:將容量分爲兩塊,每次只使用其中一塊,當其滿了以後,對其進行回收,將活着的對象複製到另一塊,順序存放。這樣解決了空間碎片問題,但會浪費掉一塊內存空間。

  現在的商業虛擬機都採用複製算法來回收新生代(Java堆分爲新生代和老年代)。通過把新生代分爲Eden區和兩個Survivor區,默認比例爲8:1:1.這樣,每次使用時,使用Eden區和一個Survivor區,在內存回收後將所有存活對象複製到另一個Survivor區,這樣空間利用率可達90%。這裏包含了一個假設:即新生代的對象大部分都活不過一個內存回收,這樣,新生代就可以良好的運行。若是內存回收後,有超過10%的對象存活,則通過分配擔保機制,將部分對象移至老年代

  針對老年代的特點:對象存活時間較長,較爲穩定。提出了標記——整理算法:在完成標記後,讓所有存活的對象都像一側移動,然後清理掉端邊界以外的內存。

  分代收集算法:將內存區域進行分塊,對不同的區域採取不同的算法。目前主流操作時把Java堆分爲新生代和老年代。對新生代採取複製算法,對老年代採取標記-整理和標記-清除算法。

1.3 什麼時候進行垃圾回收

  在上文中提到,JVM中採用可達性分析算法通過列舉GC Roots查看引用鏈來判定對象生死的。對於Java堆和方法區很大的情況下,如何快速完成GC Roots的枚舉和對象的標記極大的影響JVM性能和GC停頓時間,影響使用者體驗。現在主流的JVM使用準確式GC,通過維護OopMap數據結構來得知對象引用地址,從而完成上述工作。

  那麼,在何處生成OopMap至關重要:HotSpot中僅在安全點記錄了這個信息。當所有線程都位於安全點時,進行垃圾回收。安全點的選定以是否能讓程序長時間執行爲標準:如方法調用、循環跳轉、異常跳轉等。

  關於安全點的另一個問題是如何讓所有線程都位於安全點。這裏有兩種方法:搶先式中斷主動式中斷。目前JVM都採用主動式中斷:當GC需要中斷線程時,設置一個標誌,各個線程輪詢去訪問該標誌,發現中斷標誌爲真則自己中斷掛起。

  此外,當線程處於Sleep狀態Blocked狀態時,線程無法響應中斷請求,這樣就無法進行GC。針對這種情況,我們把其列爲安全區域,視爲對安全點的擴展,在安全區域的任何階段進行GC都是安全的。

二 垃圾收集器

  上文講述了垃圾回收的基本原理,本部分講述幾款常見的垃圾收集器。對於本部分的內容,不做詳細講解,主要搭配講解。

Serial收集器Serial Old收集器ParNew收集器

  Serial收集器和ParNew收集器這兩個收集器都是新生代收集器。其中,Serial收集器是JVM運行在Client模式下的默認新生代收集器,是單線程收集器;ParNew收集器是其多線程版本。

  Serial Old收集器是Serial收集器的老年代版本,也是單線程收集器,採用標記-整理算法。此外,在JDK1.5之前,其爲Parallel Scavenge收集器的老年代版本,也是CMS收集器的後備方案。

Parallel Scavenge收集器和Parallel Old收集器

  這套收集器組合和別的收集器不一樣的是其目標是實現最大吞吐量,而其他收集器的目標是實現最小GC停頓時間。也就是說,其適合在後臺運算不需要進行很多交互的任務。另外,採用Parallel Scavenge收集器GC自適應的調節策略:根據VM監控情況動態調整參數實現最合適的停頓時間或最大吞吐量。

  需要說明的是,在Parallel Old出來之前,Parallel Scavenge收集器只能和Serial Old搭配使用,發揮不了其優勢。

CMS收集器

  CMS收集器(Concurrent Mark Sweep)以獲取最小GC停頓時間爲目標,適合應用在網站和B/S服務端這種重視響應速度的場合。由名字可知,CMS收集器是基於併發的標記-清除收集器。其收集分爲四個過程:

  • 初始標記:標記GC Roots能關聯到的對象,速度很快,會Stop the World
  • 併發標識:進行GC Roots Tracing,耗時最長,但由於是併發進行,不會Stop the World
  • 重新標識:修正併發標記過程中的變動,時間比初始標識稍長,會Stop the World
  • 併發清除

  由上述過程可發現,在採用CMS收集器時,GC停頓只發生在初始標記和重新標識這兩個耗時較短的階段,所以GC停頓時間很低。

  下面談談CMS收集器的三大缺點:

  • 對CPU資源敏感:也就是說,在併發標記期間,CMS收集器本身是佔用系統資源的,這是沒法避免的,只能通過設計減小其佔用比例
  • CMS無法處理浮動垃圾:即在併發清理階段產生的垃圾。這一點也是無法避免的。
  • CMS基於標記-清理,會產生大量內存碎片。這點可以通過設置整理來解決大部分問題。
G1收集器

  G1收集器面向服務端,其回收過程與CMS收集器類似。使用G1收集器時,新生代和老年代無需物理隔離。具體的還是上網查資料吧,這裏不再詳細敘述。

三 內存分配和回收策略

  下面講一下幾條最普遍的內存分配規則,讀者可以自行驗證。驗證時記得查看或設置自己的垃圾回收器。

  • 大多數情況下,對象在新生代的Eden區分配。當Eden區沒有足夠空間時,進行一次Minor GC
  • 大對象(長字符串,數組等)直接進入老年代,具體多大算大可以自己設置。
  • 長期存活的對象將進入老年代:默認是經過15次Minor GC算長期,該值也可以自己重新設置
  • 動態對象年齡判定:若相同年齡的所有對象大小總和大於Survivor空間的一半,則年齡大於或等於該年齡的對象可以直接進入老年代,無需達到上述年齡閾值。
  • 空間分配擔保:老年代要保證能有足夠的空間進行安排可能從新生代移至老年代的對象,這就是空間分配擔保。這裏的問題在於:要是確保使老年代可用空間最大,則需進行Full GC,但這樣會引起GC停頓。所有需要提供策略實現其中的一個平衡。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章