一文帶你瞭解經典的Java垃圾回收機制

本文要點:

  • 分代假設是現代垃圾回收器的效率的關鍵所在;
  • HotSpot通過計算對象在回收過程中存活下來的次數來實現分代回收;
  • 並行回收器仍然是使用最爲廣泛的Java垃圾回收器;
  • 回收算法的複雜度很難進行精確的預算;
  • 壓縮回收器(如ParallelOld)的行爲與就地回收器完全不同。

在Java 8中,HotSpot虛擬機的默認垃圾回收器是ParallelOld。在Java 11中,默認回收器變成了G1。

注意:從技術上講,回收器的切換是在Java 9中進行的,但G1的主要增強是在Java 10和11中完成的。但實際上,很少有公司使用Java LTS以外的版本。

在本文中,我們將討論垃圾回收理論的一些基礎知識,以及這些理論在HotSpot中是如何實現的。這也將解釋爲什麼要切換Java的默認垃圾回收器,以及Java垃圾回收方法在近來發生的一些變化。

基本概念

垃圾回收是系統的一種“清理”活動,獨立於應用程序的主處理線程,試圖找出不再被使用的內存並將其釋放以便可以繼續重用。

Dijkstra對垃圾回收的定義清晰地指出,引用計數是自動內存管理的一種形式,但不屬於垃圾回收。

引用計數會在程序運行時更新每個對象的元數據(例如,在對一個引用類型對象的某個字段賦值時)。元數據的更新需要在應用程序線程上進行,因此不能清晰地將其劃分爲獨立的活動。

回收算法從root(一組已知是存活對象)開始,通過跟蹤指針來確定存活對象。

這些跟蹤回收器實現了圖算法,將堆內存劃分爲存活的和可回收的。

在現代垃圾回收文獻中,併發(Concurrent)和並行(Parallel)都被用來描述回收算法。它們聽起來像是同義詞,但實際上有着完全不同的含義:

  • 併發——回收線程獨立於應用程序線程運行;
  • 並行——使用多個線程來執行垃圾回收算法。

它們可以被看成是另外兩個術語的對立面——併發是stop-the-world(STW)的對立面,並行是single-thread(單線程)的對立面。

實際的垃圾回收器分爲多個階段,每個階段還可能具備多種特徵。

例如,某個階段可能是單線程併發,或者是並行STW。

注意:併發回收器比STW回收器要複雜得多。它們在計算開銷方面要大得多,而且它們的行爲還有需要注意的地方。

其他你應該知道的垃圾回收術語:

  • Exact——Exact回收器擁有足夠的類型信息,能夠區分int和指針之間的區別。
  • 驅逐(Evacuate)——移動(驅逐)存活對象到內存的另一個區域。在回收週期結束時,源內存區域變成空的,可以被重用。
  • 壓縮(Compact)——在回收週期結束時,存活的對象被連續地放在內存的前部區域,剩下的區域可被重用。

Exact是一種保守模式,缺乏精確的信息,因此通常會造成更大的內存浪費。

一些資料還提到了移動回收器——包括壓縮和驅逐算法。但這兩種類型之間的差異太大,把它們組合在一起通常用處不大。

非移動回收器被稱爲就地回收器。這些算法需要知道可用內存塊的列表才能夠處理內存碎片以及合併可用的內存塊。

HotSpot中的一些設計考慮

我們從定義開始,先來考慮一些基本的事實:

  • 移動回收器分配的對象在其生命週期期間沒有穩定的內存地址。
  • 壓縮回收器可用避免出現內存碎片。
  • 驅逐回收器也可以避免內存碎片化,並可以實現對存活對象進行部分壓縮。
  • 如果堆只由一個內存池組成,則無法使用驅逐算法回收。

分代假設基於對面向對象系統運行時行爲的觀察,它大致將對象分爲兩類:短期的臨時對象和用於執行程序任務的長期對象。

注意:分代回收器並不一定總是比非分代回收器更高效,但幾乎所有的應用程序都會從分代回收器中獲得好處。

回收算法的mark-sweep-compact(根據Blackburn和McKinley)是這樣定義的:

  • 標記(Mark):通過跟蹤對象圖來識別存活的對象。
  • 清掃(Sweep):讓存活對象留在原地,同時識別出可釋放的空間。
  • 驅逐(Evacuate):將存活對象轉移到另一個內存池,以此來釋放空間。
  • 壓縮(Compact):通過移動同一內存池中的存活對象來釋放空間。

在分代回收算法中,年輕代回收器和老年代回收器通常使用的是完全不同的算法。

這導致我們很難準確地對不同階段採用不同算法的回收器進行歸類。例如,在CMS中,年輕代是通過驅逐算法那進行回收的,而老年代是通過標記清除算法進行回收的,如果併發回收失敗(例如由於碎片),則退回到標記壓縮算法。

HotSpot中的年輕代垃圾回收

在HotSpot中,傳統的回收器將內存劃分爲4個內存池,分別是Eden、Survivor 0、Survivor 1和Tenured。前三個被統稱爲年輕代,Tenured是老年代。

年輕代空間是在年輕代回收週期中進行回收的,使用了並行STW驅逐算法,將存活的對象轉移到一個空間。

回收算法在當前活動的內存池中標記存活的對象,然後將其撤到非活動的內存池中。在回收結束時,兩個空間被顛倒過來——活動的內存池變爲非活動的(即爲空),而非活動的變爲活動的。有時候這也被叫作“半球”(hemispehric)回收。

半球回收可能會浪費內存。單遍算法無法預先知道正在回收的內存區域中有多少對象是存活的。這意味着用於存放驅逐對象的區域必須和被清理的區域一樣大——因此算法需要兩倍於實際存活對象大小的內存空間。

它還意味着不管在什麼時候都有一半的空間是空的。這些特點導致它不適用於現代工作負載的老年代垃圾回收,因爲這些老年代的對象集合可能很大:實際上,在生產環境中,HotSpot回收器不會使用半球回收算法。

半球回收算法被用於回收年輕代。它非常適用於符合分代假設的工作負載——即內存區域裏大部分都是垃圾對象。回收器受益於這樣的一個事實:存活對象總是從年輕代被提升到老年代。

驅逐回收器的另一個主要優點是它們處理空閒空間的方式。最簡單的方法是使用指向空閒空間的指針,當存活對象被驅逐時,很“自然”地被壓縮。

驅逐算法是OpenJDK年輕代回收器的典型算法,它使用了對象跟蹤。不過,回收只在一個階段中進行,沒有單獨的標記、清除或壓縮階段。

分代假設的後果

對象的生存期通常是未知的,而且在實際應用程序中會動態發生變化。因此,追蹤對象的實際生存週期是不可行的。

相反,HotSpot記錄了對象在垃圾回收過程中存活下來的次數,只需要在對象頭部的元數據裏添加幾個比特的信息,在對象經歷了足夠多的垃圾回收之後,它就會被移動(提升)到更老的一代,由不同的垃圾回收器來管理。

這種機制與應用程序的內存分配速度存在一種有趣的交互。如果分配速度加快,那麼年輕代將更快被填滿——但“短命對象”的預期壽命(以毫秒爲單位)保持不變。

這可能會導致更多對象在回收週期中存活下來,從而導致年輕代空間充滿了還沒有資格提升到老年代的對象。在這種情況下,JVM別無選擇,只能提前提升一些對象——這導致了“過早提升”。

很多這樣的對象實際上都是短命的,在進入老年代後很快就會消失。可惜的是,JVM沒有回收它們的機制,要等到老年代空間的下一個回收週期才能回收它們。

垃圾回收算法的複雜性

開發人員經常對垃圾回收算法進行複雜性分析(有時候也叫作“大O”)。然而,在實踐當中,這種做法實際上並不是很令人滿意。

他們可能天真地認爲標記和壓縮階段的時間複雜度與活動對象集合的大小成線性關係,而清除階段與整個堆大小成線性關係。

然而,即使不考慮在實際實現當中可能無法清晰地進行階段隔離(如上面討論的HotSpot年輕代回收器),仍然存在一個更深層次的問題。

垃圾回收本質上是一種通用算法。這意味着大O分析中的固有假設——當數據集增大時,起作用的是限制性行爲——是不正確的。

生產環境中的算法需要在面對所有可能的輸入和工作負載表現出可接受的行爲。它們的漸近性行爲與整體性能是不匹配的。

換句話說,活動對象集合和堆大小本質上是獨立變化的(例如,不同的對象圖拓撲)。這意味着對於不同的工作負載,縮放因子會產生非常不一樣的效果。

例如,壓縮時需要複製字節,因此,儘管壓縮階段在活動對象集合的大小上是呈線性的,但其他因素可能與要移動的對象大小有關。對於包含大量元素的大數組,這種說法就更加站不住腳。

對於各種不同形式的回收算法,還存在一些衆所周知的二級效應。例如,在對只有少量存活對象的內存區域(“稀疏堆”)執行壓縮時,活動的對象將被合併到更密集的區域。如果對象的生命週期很長,那麼這個區域對於後續的回收週期來說就不那麼稀疏了。

我們可以看到,與CMS之類的就地回收器相比,在程序的整個生命週期中,長壽對象將保持稀疏分佈。事實上,隨着時間的推移,空閒空間將變得越來越碎片化,空閒內存塊列表的管理將變得越來越昂貴。

總的來說,不同回收方法的時間和空間成本模型是不同的,簡單的算法複雜度分析也不是很管用。在HotSpot中,如果沒有足夠的連續空間,就地回收器最終會退回到壓縮回收器。

總結

我們討論了Java虛擬機的垃圾回收機制。垃圾回收是計算機科學的一個成熟的領域,HotSpot的垃圾回收器經過了良好的測試,可以很好地處理大堆工作負載。大多數Java應用程序不需要過多地擔心垃圾回收行爲。

如果對垃圾回收行爲較爲敏感,那麼深入瞭解垃圾回收的原則(以及它在JVM中是如何實現的)對於開發人員來說會很有幫助。

在最近的Java版本中,垃圾回收子系統的改進再次成爲關注的熱點。要完全理解這些變化,就要很好地掌握這些基礎知識。後續的文章將詳細討論這些更新,例如,爲什麼改變了默認回收器、這對升級到Java 11的團隊意味着什麼。

作者簡介:

Ben Evans是JVM性能優化公司jClarity的聯合創始人。他是LJC(倫敦Java用戶組)的組織者和JCP執行委員會成員。Ben是Java Champion,三次JavaOne Rockstar演講者,著有《The Well-Grounded Java Developer》、新版《Java in a Nutshell》和《Optimizing Java》。他經常發表與Java平臺、性能、架構、併發和初創公相關的演講。

原文鏈接

Understanding Classic Java Garbage Collection

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章