Java HotSpot VM內存管理白皮書

本文由作者自行翻譯,未經作者授權,不得隨意轉發。后續作者會陸續發布一系列關于JVM內存管理的文章,敬請期待。

1、介紹

Java2平臺標準版本(J2SE)的一個優點就是它的自動內存管理功能,使得開發人員與顯式的內存管理的復雜性分隔開。

本文對Sun J2SE5.0版本中Java HotSpot虛擬機(JVM)內存管理提供了一份概述。它描述了用于執行內存管理的垃圾回收器,并給出了一些關于選擇和配置垃圾回收器以及為垃圾回收器操作的內存區域設置大小的建議。它還是一份參考,列出了一些最常用的影響垃圾回收器行為的選項,并且提供了大量更詳細文檔的鏈接。

第二節針對那些對自動內存管理還是新手的讀者。它對于如此管理內存較之需要程序員顯式為數據分配空間的好處有一個簡短的討論。

第三節介紹了一般垃圾回收器的概念、設計選擇以及性能指標。它還介紹了一種常用的基于對象期望存活時間將內存分為不同區域的組織形式,稱為“代”。這種分離為“代”的方式已被證明在減小廣泛范圍內應用的垃圾回收暫定時間以及總體成本方面是有效的。

文章的剩余部分提供了針對HotSpot JVM的信息。第四節描述了四個有效的垃圾回收器,包括一個在J2SE5.0 update 6中新引入的,記錄了它們使用的“代”內存組織。對于每個垃圾回收器,第四節概述了使用的回收算法類型,并具體說明什么時候適合選擇這個回收器。

第五節描述了一項在J2SE5.0新引入的技術,結合(1)基于應用運行的平臺和操作系統,自動選擇垃圾回收器、堆大小以及HotSpot JVM(客戶端或者服務器),以及(2)基于用戶指定的期望行為動態垃圾回收調優。這項技術成為人類工程學。

第六節提供了選擇和配置垃圾回收器的建議。它還提供了一些建議如如何應對OutOfMemoryError異常。第七節簡略的描述了一些工具,可以用于評估垃圾回收性能,第八節列出了大多數常用的與垃圾回收器選擇和行為相關的命令行選項。最后,第九節提供了本文章包括的各種主題更詳細文檔的鏈接。

2、顯式VS自動內存管理

內存管理是識別什么時候已分配對象不再需要,并釋放這些對象使用的內存,使其可用于后續分配的過程。在一些程序語言中,內存管理是程序員的職責。該任務的復雜性導致許多常見的錯誤,這些錯誤可能導致意外的或者錯誤的程序行為和崩潰。因此,開發人員的大部分時間通常用于調試和嘗試糾正此類錯誤。

在顯示內存管理的程序中經常發生的一個問題是懸空引用(dangling references)。它可能重新分配一個對象使用的空間,而這個對象仍被一些其它對象引用。如果擁有這樣引用(dangling)的對象嘗試訪問原始對象,但是空間已經被重新分配給了一個新的對象,結果是不可預知的而不是預期的。

顯示內存管理的另一個常見問題是內存泄露。這些泄露在內存已被分配而且不再被引用但未被釋放的時候發生。例如,如果你打算釋放一個鏈表使用的空間但是你錯誤的只釋放了列表的第一個元素,剩余的列表元素不再被引用但是它們已經脫離了程序的范圍,既不再被使用也不會被恢復。如果發生足夠的泄露,它們可能持續消耗內存,直到用盡所有有效內存為止。

現在常用的一種內存管理的替代方法,特別是大多數現代面向對象語言使用的,是由一個稱為“垃圾回收器”的程序自動管理。自動內存管理可以增強接口和更可靠的代碼抽象。

垃圾回收避免了懸空引用的問題,因為一個仍然引用的對象永遠不會被垃圾回收,因此將不會被視為釋放。垃圾回收還解決了上面描述的內存泄露的問題,由于它自動釋放所有不再引用的內存。

3、垃圾回收概念

一個垃圾回收器負責:

  • 分配內存
  • 確保任何引用對象保持在內存中
  • 并且回收執行代碼中不再引用的對象使用的內存

依然被引用的對象被認為是活的。不再被引用的對象被認為是死的,被稱為垃圾。發現并釋放(也稱為回收)這些對象所使用的內存的過程稱為垃圾回收。

垃圾回收解決了許多,但不是全部的內存分配問題。例如,你可以無限期的創建對象并持續引用它們,直到沒有更多可用內存為止。垃圾回收也是一項復雜的任務,需要耗費它自身的時間和資源。

用于組織內存、分配和釋放空間的精確算法由垃圾回收器處理,對程序員隱藏。空間通常是從一個稱為“堆”的內存池中分配的。

什么時間執行垃圾回收取決于垃圾回收器。通常,當堆被填滿或者到達一個占用的閾值百分比時,整個堆或它的一部分被回收。

執行一個分配請求的任務,即在堆中找到一定大小的未使用的內存塊,是一項困難的任務。對于大多數動態內存分配算法來說,主要問題是在保持分配和釋放效率的同時避免碎片(見下文)。

令人滿意的垃圾回收器特征

一個垃圾回收器必須既安全又全面。也就是說,活著的數據必須永遠不會被錯誤的釋放,垃圾不應該被排除在幾個回收周期之外仍未被回收。

垃圾回收器還應該高效的運行,不會引入長時間的暫停(在這期間應用程序不能運行)。然而,與大多數計算相關的系統一樣,這常常存在時間上、空間上以及頻率上的權衡。例如,如果一個堆比較小,回收會很快,但是堆也會更快的填滿,因此需要更頻繁的回收。相反,一個大的堆將耗費更長時間填滿,因此回收會不那么頻繁,但是它們可能需要更長的時間。

另一個可取的垃圾回收器的特征是碎片的限制。當釋放垃圾對象的內存時,空閑空間可能在各個區域中以小塊的形式出現,導致在任何一個連續區域中都沒有足夠的空間用于分配給一個大對象。消除碎片的一種方法稱為“壓縮”,它將在接下來的各種垃圾回收器設計選中討論。

伸縮性也很重要。分配不應成為多處理器系統上多線程應用的可伸縮性瓶頸,回收也不應該稱為瓶頸。

設計選擇

在設計或選擇垃圾回收算法時,必須做出許多選擇:

  • 串行 VS 并行

對于串行回收,在一個時刻只發生一件事情。例如,即使有多個CPU可用,也只使用一個來執行回收。當使用并行回收時,垃圾回收的任務被拆分成幾部分,這些部分在不同的CPU上同時執行。同時操作可以更快的完成回收,以一些額外的復雜性和潛在的碎片為代價。

  • 并發 VS Stop-the-world

當stop-the-world垃圾回收執行時,在回收期間,應用程序的執行完全暫停。或者,可以同時執行一個或者多個垃圾回收任務,即與應用程序同時執行。通常,一個并發垃圾回收器并發執行大部分工作,但是偶爾也可能需要做一些短暫的stop-the-world暫停。stop-the-world垃圾回收比并發回收更簡單,因為回收期間,堆被凍結,對象沒有改變。它的缺點是對某些應用程序來說暫停可能是不可取的。相對的,當垃圾回收并發執行時,暫停時間會更短,但回收器必須格外小心,因為它操作的對象可能同時在被應用程序更新。這增加了并發回收器的開銷,會影響性能,需要更大的堆。

  • 壓縮 VS 非壓縮 VS 復制

垃圾回收器確定內存中哪些對象是存活的以及哪些是垃圾之后,它可以壓縮內存,將所有存活的對象移到一起并且完全回收剩余內存。壓縮后,很容易快速在第一個空閑的位置分配一個新對象。一個簡單的指針可以用來跟蹤下一個可用于對象分配的位置。相比一個壓縮回收器,一個非壓縮回收器就地釋放垃圾對象使用的空間,換言之,它不會像壓縮回收器一樣移動所有存活的對象以創建一個大的再生區。它的好處是更快的完成垃圾回收,缺點是潛在的碎片。總得來說,從一個就地回收的堆中分配內存比從一個壓縮堆要更昂貴。它可能需要在堆中查找一個足夠大的連續的內存區域來容納新對象。第三選擇是復制回收器,它拷貝(或搬出)存活對象到一個不同的內存區域。它的好處是源區域接下來可以被認為是空的,可用于后續快速和容易的分配,但是缺點是需要額外的拷貝時間,以及需要額外的空間。

性能指標

  • 吞吐量——考慮在長時間內,沒有花費在垃圾回收上的總時間的百分比。
  • 垃圾回收的開銷——與吞吐量想反,即花費在垃圾回收上的總時間的百分比。
  • 暫停時間——當垃圾回收發生時,應用程序執行停止的時間長度。
  • 回收頻率——回收發生的頻率,相對于應用程序執行。
  • 空間量(Footprint)——一個尺寸的度量,如堆大小
  • 及時性(Promptness)——一個對象變為垃圾到內存變為可用的時間

一個交互應用可能需要較低的暫停時間,而總的執行時間對一個非交互應用則更重要。一個實時(real-time)應用要求在任何時期內垃圾回收暫停時間以及回收器花費的時間比都有很小的上界。一個小的空間量(footprint)可能是在小型的個人計算機或者嵌入式系統中運行的應用程序的主要關注點。

分代回收

當使用一項稱為“分代回收”的技術時,內存被分為若干“代”,即容納不同年齡對象的獨立的池。例如,最廣泛使用的配置有兩代:一個用于年輕對象,一個用于年老對象。

可以使用不同的算法在不同的代執行垃圾回收,每個算法優化基于該特定代的常見的特征。就使用幾種編程語言包括Java語言編寫的應用程序而言,分代垃圾回收利用以下觀測結果,稱為弱代假設:

  • 大多數分配的對象不會被長期引用(被認為是存活的),也就是說,它們年輕就死了。
  • 存在很少的從較老對象對較年輕對象的引用。

年輕代回收發生相對頻繁,效率高、速度快,因為年輕代空間通常很小并且可能包含大量不再被引用的對象。

經歷多次年輕代垃圾回收幸存的對象最終提升到年老代。圖1所示,年老代通常比年輕代更大,其占用率增長較慢。因此,年老代回收較少,但需要花費更長的時間才能完成。

圖1.代垃圾回收

對年輕代垃圾回收算法的選擇通常強調速度,由于年輕代回收頻繁。另一方面,年老代通常由一個空間利用率更高的算法來管理,因為年老代占據了堆的大部分,年老代算法必須在垃圾密度比較低的情況下很好的工作。

4、J2SE5.0 HotSpot JVM中的垃圾回收器

截止J2SE 5.0 update 6,Java HotSpot虛擬機包含四個垃圾回收器。所有回收器都是分代的。本節描述了回收器的代和類型,并討論了為什么對象分配通常快速并高效。然后提供了每個回收器的詳細信息。

HotSpot的代

Java HotSpot虛擬機中的內存分為三代:年輕代、年老代和一個永久代(譯者注:永久代在J2SE 8.0中已經廢棄,引入了元空間)。大多數對象最初在年輕代被分配。年老代容納了經過幾次年輕代回收幸存下來的對象,以及一些大對象可能直接在年老代被分配。永久代持有JVM發現便于讓垃圾回收器管理的對象,諸如描述類和方法的對象,以及類和方法本身。

年輕代由一個稱為伊甸(Eden)的區域加上兩個較小的幸存(Survivor)空間組成,如圖2所示。大多數對象最初在伊甸區分配(如前所述,一些大的對象可能直接在年老代分配)。幸存空間持有至少一次年輕代回收之后幸存的對象,因此在被認為“足夠老”以致提升到年老代之前被賦予了額外的機會死亡。在任何給定的時間,幸存空間(圖中標為From)中的一個持有這些對象,而另一個是空的,保持未使用直到下一次回收。

圖2.年輕代內存區域

垃圾回收類型

當年輕代被填滿后,一次只包含該代的年輕代回收(有時稱為Minor GC)會被執行。當年老代或者永久代被填滿,通常稱為完全垃圾回收(有時稱為Major GC)的操作被執行。也就是說,所有代會被回收。通常,年輕代先回收,使用專門為該代設計的回收算法,因為它通常是識別年輕代垃圾的最高效的算法。然后接下來提到的年老代回收算法是在年老代和永久代上運行。如果發生壓縮,則每一代分別壓縮。

如果年輕代先回收,有時年老代太滿以致不能接受所有應該從年輕代提升到年老代的對象。在這種情況下,對于除CMS之外的回收器,年輕代回收算法不會運行。相反,年老代回收算法在整個堆被使用(CMS年老代算法是一個特例,因為它不能回收年輕代)。

快速分配

正如你將在下面的垃圾回收器描述中看到的,在許多情況下,有大量的連續內存塊可以用來分配對象。這樣的塊的分配是高效的,使用一項簡單的bump-the-pointer技術。即總是保持對上一次分配對象的結束位置的追蹤。當需要滿足一個新的分配請求時,所有需要做的是檢查對象是否適合代的剩余部分,如果是,更新指針并初始化對象。

對于多線程應用,分配操作需要是多線程安全的。如果使用全局鎖來確保這一點,那么分配到一個代將稱為瓶頸并降低性能。相反,HotSpot JVM采用一項稱為線程本地分配緩沖區(Thread-Local Allocation Buffers:TLABs)的技術。通過給每個線程自己的緩沖區(即代的一小部分)來分配,從而提高了多線程分配的吞吐量。由于只有一個線程可以被分配到每個TLAB,分配可以使用bump-the-pointer技術快速完成,無需任何鎖定。只有偶爾當一個線程填滿它的TLAB并需要獲得一個新的時,必須同步進行。由于TLAB的使用,幾項減小空間浪費的技術被使用。例如,TLAB由分配器分配尺寸平均浪費少于伊甸區的1%。TLAB與利用bump-the-pointer技術的線性分配的使用組合使每次分配都是高效的,只需要10個左右的本地指令。

串行回收器

使用串行回收器,年輕代和年老代回收都是串行完成的(使用單個CPU),以stop-the-world的方式。即,應用程序執行在回收發生時被停止。

使用串行收集器的年輕代回收

圖3演示了一次使用串行回收器的年輕代回收的操作。伊甸區的存活對象被拷貝到最初為空的幸存空間,圖中表為To,除了那些太大而不適合于放到To中的對象。這些對象直接被拷貝到年老代。在已使用的幸存空間(標為From)中的存活對象中,仍舊相對年輕的也被拷貝到另一個幸存空間(即To),而相對較老的對象被拷貝到年老代。注意:如果To空間已滿,未被拷貝它里面的來自伊甸區或From空間的存活對象是長期持有的(拷貝到年老代),不管它們經歷了多少次年輕代垃圾回收而存活下來。在存活對象已被拷貝后,仍存在于Eden和From空間中的任何對象,按照定義,是非存活的,它們不需要檢查(這些垃圾對象在圖中標記了一個X,雖然事實上回收器不會檢查或標記這些對象)。

圖3.串行年輕代回收

在一次年輕代回收完成之后,伊甸區和原先被占用的幸存空間是空的,只有原先空的幸存空間包含存活對象。此時,存活空間互換角色。見圖4。

圖4.一次年輕代回收之后

使用串行回收器的年老代回收

使用串行回收器,年老代和永久代通過一個標記-清除-壓縮(mark-sweep-compact)的回收算法回收。在標記階段,回收器標識哪些對象依舊存活。清除階段“掃視”整個代,識別垃圾。回收器然后執行平移壓縮,將存活對象移到年老代空間的開始(永久代類似),在另一端使得任何空閑空間成為一個單獨的連續塊。見圖5。該壓縮允許任何將來到年老代或者永久代的分配可以使用快速bump-the-pointer技術。

圖5.年老代壓縮

什么時候使用串行回收器

串行回收器是那些運行于客戶機上的、不需要較低暫停時間的大多數應用的選擇回收器。在今天的硬件,串行回收器可以有效的管理大量的非平凡的應用,使用64MB堆和相對短的完全回收低于半秒的最壞情況的暫停。

串行回收器選擇

J2SE5.0發行版中,在非服務器類型的機器上,串行收集器自動被選擇作為默認的垃圾回收器,如第5節所述。在其它機器上,串行回收器可以通過使用命令行選項-XX:+UseSerialGC來顯式請求。

并行回收器

現在,許多Java應用運行在擁有大量物理內存和多個CPU的機器上。并行回收器,也稱為吞吐量回收器,被開發用來充分利用有效的CPU,而不是讓它們大多數空閑而只有一個執行垃圾回收工作。

使用并行垃圾回收器的年輕代回收

并行回收器使用了串行回收器年輕代回收算法的一個并行版本。它仍然是一個stop-the-world以及拷貝回收器,但是以并行的方式執行年輕代回收,使用多個CPU,減少垃圾收集開銷,從而增加應用程序吞吐量。圖6演示了對于年輕代,串行回收器和并行回收器的不同。

圖6.串行和并行年輕代回收對比

使用并行回收器的年老代回收

對于并行回收器,年老代垃圾回收使用與串行回收器相同的串行標記-清除-壓縮的回收算法。

什么時候使用并行回收器

可以從并行回收器獲益的應用程序是那些運行在多個CPU的機器上并且沒有暫停時間約束的應用,因為不經常的但是可能很長的年老代回收將仍然會出現。并行回收器通常合適的應用程序示例包括批處理、計費、工資、科學計算等等。

你可能需要考慮在并行回收器之上選擇并行壓縮回收器(下面描述),因為前者執行所有代的并行回收,而不僅僅是年輕代。

并行回收器選擇

J2SE5.0發行版中,并行回收器在自動被選擇作為服務器類型(在第5節定義)機器上的默認垃圾回收器。在其它機器上,并行回收器可以被顯式的請求,通過使用-XX:+UseParallelGC命令行參數。

并行壓縮回收器

并行壓縮回收器在J2SE 5.0 update 6中被引入。它與并行回收器的不同是它使用一個新的算法進行年老代垃圾回收。注意:最終并行壓縮回收器將取代并行回收器。

使用并行壓縮回收器的年輕代回收

對于并行壓縮回收器,年輕代垃圾回收使用與并行回收器年輕代垃圾回收相同的算法完成。

使用并行壓縮回收器的年老代回收

使用并行壓縮回收器,年老代和永久代在一個stop-the-world中被回收,平移壓縮大多是并行的。回收器使用三個階段。首先,每個代在邏輯上劃分為固定大小的區域。在標記階段,直接從應用程序代碼可訪問的存活對象初始集合被劃分到垃圾回收線程中,然后所有存活對象被并行標記。當一個對象識別為存活,它所在區域的數據隨這個對象的大小和位置信息更新。

摘要(summary)階段操作區域,而不是對象。由于先前回收的壓縮,通常每個代的左側部分將是密集的,包含絕大多數存活對象。可以從這些密集區域恢復的空間量不值得壓縮它們的成本。因此摘要階段做的第一件事情是檢查區域的密度,從最左邊的開始,直到它到達一個點,可以從一個區域以及那些它右側的區域恢復的空間值得壓縮這些區域的成本。這個點左側的區域被稱為dense prefix,沒有對象被移動到這些區域。這個點右側的區域將被壓縮,消除所有死空間。摘要階段計算并存儲每個壓縮區域的存活數據的首字節的新位置。注意:摘要階段目前作為一個串行階段實現;并行化是可能的,但是對性能不像標記和壓縮階段并行化一樣重要。

在壓縮階段,垃圾回收線程使用摘要數據識別需要填充的區域,并且各線程可以獨立的將數據拷貝到區域中。這會在一端產生一個密集填充的堆,而在另一端有一個大的空閑塊。

什么時候使用并行壓縮回收器

與并行回收器一樣,并行壓縮回收器對于運行在多個CPU的機器上的應用是有益的。另外,年老代回收的并發操作降低了暫停時間,使得并行壓縮回收器比并行回收器更適合于有暫停時間約束的應用。并行壓縮回收器可能不適合于運行在大型共享設備(如SunRays)上的應用,這類設備上沒有單一應用可能長期獨占幾個CPU,可以考慮減少用于垃圾回收的線程數(通過-XX:ParallelGCThreads=n命令行參數)或者選擇一個不同的回收器。

并行壓縮回收器選擇

如果你想要使用并行壓縮回收器,你必須通過指定命令行選項-XX:+UseParallelOldGC來選擇它。

并發標記清除回收器(CMS)

對于大多數應用程序來說,端到端吞吐量不如快速響應時間重要。年輕代回收器通常不會引起長時間暫停。然而,年老代回收,雖然比較少,但可能會造成長時間暫停,尤其當涉及大的堆時。為了解決這個問題,HotSpot JVM包含了一個稱為并發標記清除(CMS)的回收器,也稱為低延遲回收器。

使用CMS回收器的年輕代回收

CMS回收器回收年輕代的方式與并行回收器相同。

使用CMS回收器的年老代回收

使用CMS回收器的年老代回收的大部分與應用程序執行并發執行。

CMS回收器的一個回收周期從一個短暫的暫停開始,稱為初始標記,它識別從應用程序代碼直接可達的存活對象的初始集合。然后,在并發標記階段,回收器標記那些從初始集合間接可達的所有存活對象。由于標記階段執行時,應用程序也正在運行和更新引用字段,所以并非所有存活對象都能保證在并發標記階段結束時被標記。為了解決這個問題,應用程序再次暫停,稱為再次標記(remark),它通過再次訪問在并發標記階段被修改的任何對象來完成標記。由于再次標記(remark)暫停比初始標記更大,因此并行運行多個線程以提高其效率。

在再次標記(remark)階段結束時,堆中的所有存活對象都保證已被標記,因此后續并發清除階段回收所有那些已識別的垃圾。圖7演示了年老代回收使用串行標記清除壓縮(mark-sweep-compact)回收器與CMS回收器的不同。

image.png

圖7.串行和CMS年老代回收對比

由于某些任務,如再次標記(remark)階段的對象重新訪問,增加了回收器必須做的工作量,它的開銷也會隨之增加。這對于大多數試圖減少暫停時間的回收器來說是一種典型的折中方案。

CMS回收器是唯一的非壓縮回收器。也就是說,它釋放了死對象占用的空間之后,不會移動存活對象到年老代的一端。見圖8。

圖8.年老代的CMS清除(但是不壓縮)

這節省了時間,但是由于空閑空間不是連續的,回收器不能再使用一個簡單的指針來指示可以用于下一個對象分配的空閑位置。相反,現在它需要使用空閑列表。更確切的說,它創建了一些列表將內存未分配區域鏈接到一起,每次一個對象需要分配時,必須查找合適的列表(基于需要的內存數量),找到一個足夠容納這個對象的區域。因此,年老代中的分配比簡單的bump-the-pointer技術更昂貴。這也給年輕代回收帶來額外開銷,因為年老代中的大多數分配是在年輕代回收期間對象提升到年老代時發生的。

CMS回收器另一個缺點是需要比其它回收器更大的堆。考慮到在標記階段運行應用程序運行,它可能繼續分配內存,從而可能持續的增加年老代。此外,雖然在標記階段回收器確保識別所有存活對象,但某些對象在該階段期間可能會變為垃圾,它們將不能被回收,直到下一次年老代回收。這些對象被稱為漂浮垃圾。

最后,由于缺少壓縮,可能會產生碎片。為了處理碎片,CMS回收器跟蹤熱門對象大小,評估將來的需求,可以拆分或合并空閑塊來滿足需求。

與其它回收器不同,CMS回收器在年老代被填滿時不會啟動一個年老代回收。相反,它試圖盡早啟動回收,以便它可以在這發生之前完成。否則,CMS回收器就會轉變成比并行和串行回收器使用的stop-the-world標記清除更耗時。為了避免這種情況,CMS回收器基于以前回收時間統計數據和年老代被占用速度的一個時間啟動。如果年老代的占有率已超過啟動占有率,CMS回收器也將啟動一次回收。啟動占用率的值通過命令行選項-XX:CMSInitiatingOccupancyFraction=n設置,其中n是年老代大小的一個百分比。默認為68。

總之,與并行回收器相比,CMS回收器降低了年老代暫停——有時明顯——以犧牲年輕代較長的暫停、一些吞吐量的減少以及額外的堆大小需求為代價。

增量模式

CMS回收器可以在一種模式中使用,其中并發階段是增量完成的。這種模式旨在通過周期性停止并發階段并回到應用程序處理的方式減輕長并發階段的影響。回收器所做的工作被拆分為在年輕代回收之間調度的小的時間塊。當需要并發回收器提供的低暫停時間的應用程序運行在少量處理器(如1個或2個)的機器上時,這個特征非常有用。使用這種模式的更多信息,參見第9節提到的“在JVM 5.0中調整垃圾回收”文章。

什么時候使用CMS回收器

如果你的應用程序需要較短的垃圾回收暫停并且能夠在應用程序運行時與垃圾回收器共享處理器資源,可以使用CMS回收器。(由于它的并發性,CMS回收器在回收周期內,將CPU周期與應用程序分開)通常,具有一份相對較大長久存活數據(一個大的年老代)的應用程序,并運行在兩個或者更多處理器的機器上,往往受益于此收集器的使用。Web服務器就是一個例子。對于任何具有低暫停時間要求的應用程序都應該考慮CMS回收器。它也可以在單個處理器上以中等大小年老代為交互式應用提供良好的結果。

CMS回收器選擇

如果你想使用CMS回收器,你必須顯示的通過指定命令行選項-XX:+UseConcMarkSweepGC來選擇。如果你想要它運行在增量模式下,也需要通過–XX:+CMSIncrementalMode選項啟用。

5、人類工程學——自動選擇和行為調整

在J2SE5.0版本中,基于應用程序運行的平臺和操作系統自動選擇垃圾回收器、堆大小以及HotSpot虛擬機(客戶端或服務器)的默認值。這些自動選擇更好地滿足不同類型應用程序的需求,同時與以前版本相比,命令行選項更少 。

此外,為并行垃圾回收器增加了動態調整回收的新方法。通過這種方法,用戶指定期望的行為,垃圾回收器動態地調整堆區域的大小,以達到所要求的行為。平臺依賴的默認選擇與使用期望行為的垃圾回收調整的組合稱為人類工程學。人類工程學的目標是使用最少的命令行調整從JVM提供良好的性能。

回收器、堆大小以及虛擬機的自動選擇

服務器類型機器被定義為:

  • 兩個或者多個物理處理器以及
  • 2GB或更多GB物理內存

這個服務器類型機器的定義適用于所有平臺,除了運行Windows操作系統的32位平臺。

在非服務器類機器上,JVM、垃圾回收器和堆大小默認值為:

  • 客戶端JVM
  • 串行垃圾回收器
  • 初始堆大小4MB
  • 最大堆大小64MB

在服務器類機器上,JVM總是服務器JVM,除非你顯式指定-client命令行參數來請求客戶端JVM。在服務器類機器上運行服務器JVM,默認垃圾回收器是并行垃圾回收器。否則,默認是串行垃圾回收器。

在運行使用并行垃圾回收器的JVM(客戶端或服務器)的服務器類機器上,默認初始和最大堆大小是:

  • 初始堆大小是物理內存的1/64,最多1GB(注意,最小初始堆大小是32MB,因為服務器類型機器定義為至少2GB內存,2GB的1/64是32MB)。
  • 最大堆大小(Maximum)是物理內存的1/4,最多1GB。

除此以外,使用與非服務器類型機器相同的默認大小(4MB初始堆大小和64MB最大堆大小)。默認值可以被命令行選項覆蓋。相關選項如第8節所示。

基于行為的并行回收調整

在J2SE5.0版本中,一個新的調整方法被添加到并行垃圾回收器,基于應用程序對垃圾回收的期望行為。命令行選項用于根據最大暫停時間和應用程序吞吐量目標指定期望行為。

最大暫停時間目標

最大暫停時間目標通過以下命令行選項指定:

-XX:MaxGCPauseMillis=n

它被解釋為對并行回收器的一個提示,即期望n毫秒或者更少的暫停時間。并行回收器將調整堆大小和其它垃圾回收相關的參數,以求保持垃圾回收暫停時間小于n毫秒。這些調整可能導致垃圾回收器降低應用程序總體吞吐量,在某些情況下,無法滿足期望暫停時間目標。

最大暫停時間目標分別應用到每一代。通常,如果沒有達到目標,該代會變得更小以求滿足目標。默認情況下沒有設置最大暫停時間目標。

吞吐量目標

吞吐量目標根據垃圾回收所花費時間和垃圾回收以外的時間(稱為應用程序時間)來衡量的。目標由命令行選項指定。

垃圾收集時間與應用程序時間的比率為

1 / (1 + n)

例如-XX:GCTimeRatio=19設置垃圾回收占總時間的5%的目標。默認目標是1%(即n=99)。花費在垃圾回收上的時間是所有代的總時間。如果沒有達到吞吐量目標,代的大小會增加,為了增加應用程序在回收之間運行的時間。更大的代需要更多的時間來填充。

占用空間的目標

如果吞吐量和最大暫停時間的目標已滿足,垃圾回收器將減小堆大小,直到無法滿足其中的一個目標(總是吞吐量目標)。未滿足的目標接下來解決。

目標優先級

并行垃圾回收器首先嘗試滿足最大暫停時間目標。只有滿足它后,它們才處理吞吐量目標。同樣,只有前面兩個目標滿足后才考慮占用空間目標。

6、建議

前一節中描述人類工程學帶來垃圾回收器、虛擬機和堆大小的自動選擇,這對于大多數應用程序來說是合理的。因此,選擇和配置一個垃圾回收器的最初建議是什么也不做!也就是說,不要指定使用某個特定的垃圾回收器等。讓系統根據你的應用程序運行的平臺和操作系統做出自動選擇。然后測試你的應用。如果它的性能是可接收的,具有足夠高的吞吐量和足夠低的暫停時間,那么你就完成了。你不需要對垃圾回收器選項進行診斷和修改。

另一方面,如果你的應用程序似乎有與垃圾回收相關的性能問題,那么你可以做的最簡單的事情首先是鑒于你的應用程序和平臺特性,考慮默認選擇的垃圾回收器是否合適。如果否,顯式選擇你認為合適的回收器,并查看性能是否變得可接受。

你可以使用如第7節描述的那些工具來度量和分析性能。基于結果,你可以考慮修改選項,諸如控制堆大小或者垃圾回收行為的選項。一些最常用的選項參見第8節。請注意:性能調優最好的方法是先測量,然后調整。使用與你的代碼實際使用有關的測試來測量。此外,謹防過度優化,因為應用程序數據集、硬件等等——甚至垃圾回器收實現!——可能隨時間而改變。

本節提供有關選擇垃圾回收器和指定堆大小的信息。然后提供調整并行垃圾回收器的建議,并給出了一些關于OutOfMemoryError如何處理的建議。

什么時候選擇一個不同的垃圾回收器

第4節講了對于每個回收器,推薦使用該回收器的情況。第5節描述了默認自動選擇串行或者并行回收器的平臺。如果你的應用程序或環境特性不同于默認回收器,那么通過以下命令行選項中的一個顯示的請求回收器:

  • –XX:+UseSerialGC
  • –XX:+UseParallelGC
  • –XX:+UseParallelOldGC
  • –XX:+UseConcMarkSweepGC

堆大小

第5節講了默認初始堆及最大堆的大小是多少。這些尺寸可能適合許多應用,但是如果你的性能問題分析(見第7節)或者一個OutOfMemoryError(在后面小節討論)表明特定代或者整個堆的大小的問題,你可以通過第8節中指定的命令行選項修改尺寸。例如,非服務器類機器上的默認64MB的默認最大堆尺寸往往太小,因此你可以通過-Xmx選項指定一個較大的尺寸。除非你遇到長時間暫停的問題,嘗試向堆授權盡可能多的內存。吞吐量與可用內存量成正比。有足夠的可用內存是影響垃圾回收性能的最重要因素。

在決定了你可以給整個堆的內存總量之后,你然后可以考慮調整不同代的大小。影響垃圾回收性能的第二大因素是年輕代在堆中所占的比重。除非你發現過多的年老代垃圾回收或者暫停時間的問題,授權年輕代充足的內存。然而,當你正在使用串行回收器時,給年輕代的內存不要超過堆的總大小的一半。

當你正使用一個并行垃圾回收器,最好指定期望行為,而不是精確的堆大小值。讓回收器自動并動態的修改堆大小,以滿足該期望行為,如下所述。

并行回收器的調整策略

如果選擇的垃圾回收器(自動或者顯式)是并行垃圾回收器或并行壓縮垃圾回收器,那么繼續指定一個對你的應用程序來說足夠的吞吐量目標(見第5節)。不要為堆選擇一個最大值,除非你知道你需要堆大于默認的最大堆大小。堆將增長或者縮小到一個尺寸來支撐選擇的吞吐量目標。在初始化以及應用程序行為變化期間堆大小的一些振蕩是可以預料的。

如果堆增長到最大值,在大多數情況下,這意味著在這個最大值內,吞吐量目標不能被滿足。將最大值設置為接近平臺總體物理內存大小但是不會導致應用程序交換的值。再次執行應用程序。如果吞吐量目標仍未達到,那么應用程序時間的目標對于平臺上的可用物理內存來說便太高了。

如果吞吐量目標可以被滿足,但是有太長的暫停,選擇一個最大暫停時間目標。選擇一個最大暫停時間目標可能意味著你的吞吐量目標將無法滿足,因此選擇一個對應用程序可接收的折中值。

當垃圾回收器視圖滿足相互競爭的目標時,堆的大小會發生震蕩,即使應用程序已經達到穩定狀態。實現吞吐量目標(可能需要更大的堆)的壓力與最大暫停時間和最小占用量目標(兩者都可能需要較小的堆)競爭。

如何處理OutOfMemoryError

大多開發者必須解決的一個常見問題是由java.lang.OutOfMemoryError導致終止的應用程序。當沒有足夠的空間用于分配一個對象時,會拋出該異常。也就是說,垃圾回收不能提供更多可用的空間來容納新對象,并且堆不能進一步擴展。一個OutOfMemoryError錯誤并不一定意味著內存泄漏。這個問題可能只是一個配置問題,例如,如果指定堆大小(或者默認堆大小,如果沒指定)對應用程序來說是不夠的。

診斷OutOfMemoryError的第一步是檢查完整的錯誤信息。在異常信息中,更多信息是在“java.lang.OutOfMemoryError”之后提供的。下面是一些常見例子,關于額外信息可能是什么、它可能意味著什么以及如何處理:

  • Java heap space

這表明不能在堆中分配。這個問題可能只是一個配置問題。你可能得到該錯誤,例如,如果由-Xmx命令行參數指定(或者默認選擇)的最大堆大小對應用程序來說是不足的。它還可能表示不再需要的對象不能被垃圾回收,因為應用程序無意的持有對它們的引用。HAT工具(見第7節)可以用來查看所有可訪問對象,并了解哪些引用使每個對象都存活。這個錯誤另一個潛在的來源可能是應用程序對finalizer的過度使用,諸如調用finalizer的線程不能跟上finalizer隊列的增加速度。JConsole管理工具可用于檢測即將終結的對象數目。

  • PermGen space

這表明永久代已滿。如前所屬,這是JVM存儲它的元數據的堆區域。如果一個應用加載了大量的類,那么永久代可能需要增加。你可以通過指定命令行選項-XX:MaxPermSize=n來完成,其中n指分配大小。

這表明應用程序試圖分配一個大于堆內存大小的數組。例如,應用程序嘗試分配一個512MB的數組,但是堆的最大值為256MB,那么將拋出這個錯誤。在大多數情況下,這個問題很可能是堆太小或者BUG導致應用程序嘗試創建一個數組,它的大小因計算錯誤而異常巨大。

第7節描述的一些工具可以用來診斷OutOfMemoryError問題。對于這項任務,一些最有用的工具是HAT(Heap Analysis Tool)、JConsole管理工具,以及jmap工具與-histo選項。

7、評估垃圾回收性能的工具

可以使用各種診斷和監控工具評估垃圾回收性能。本節提供了它們中一些的簡要介紹。有關更多信息,請參見第9節中的“工具和故障排除”鏈接。

–XX:+PrintGCDetails 命令行選項

獲得垃圾回收初始信息的最簡單方法其中之一是指定–XX:+PrintGCDetails命令行選項。對于每一次回收,這將導致信息的輸出,諸如各代垃圾回收前后存活對象的大小、各代總的可用空間、回收花費的時間長度。

–XX:+PrintGCTimeStamps 命令行選項

如果使用了命令行選項-XX:+PrintGCDetails,除了輸出的信息之外,在每次回收開始時輸出一個時間戳。這個時間戳可以幫助你將垃圾回收日志與其他日志事件關聯起來。

jmap

jmap是一個包含在JDK Solaris和Linux(不包括Windows)版本中的命令行工具。它為正在運行的JVM或者核心文件打印內存相關的統計信息。如果使用它時不添加任何命令行選項,那么它打印加載的共享對象列表,類似于Solaris pmap工具的輸出。更具體的信息,可以使用-heap、-histo或者-permstat選項。

-heap選項用于獲取包括垃圾回收器名稱、特定算法的細節(諸如用于并行垃圾回收的線程數目)、堆配置信息和堆使用摘要信息。

-histo選項可以用于獲得一個堆的與類相關的圖。對于每個類,它打印了堆中實例的數量、這些對象消耗的以字節為單位的內存總量、完全限定類名。當試圖了解堆的使用情況時,這個圖非常有用。

配置永久代的大小對于動態生成并加載大量類的應用程序(例如,JSP和Web容器)是非常重要的。如果一個應用加載了“太多”的類,那么會拋出一個OutOfMemoryError異常。jmap的-permstat選項可以用于獲取永久代中對象的統計信息。

jstat

jstat工具使用了HotSpot JVM內置的工具來提供應用程序運行相關的性能和資源消耗信息。該工具可以用于診斷性能問題,特別是與堆大小和垃圾回收相關的問題。它的許多選項可以打印關于垃圾回收行為、各代容量和使用情況的統計數據。

HPROF: Heap分析器

HPROF是JDK5.0提供的一個簡單的分析器代理。它是一個動態鏈接庫,使用JVM TI與JVM相互聯系。它以ASCII或者二進制格式輸出剖析信息到文件或者套接字。這個信息可以由一個分析器前端工具進一步處理。

HPROF能夠呈現CPU使用率、堆分配統計以及監控競爭情況。除此之外,它可以輸出完整的堆dump并報告JVM中所有監控和線程的狀態。HPROF在分析性能、鎖競爭、內存泄露以及其它問題時非常有用。HPROF文檔的鏈接見第9節。

HAT: 堆分析工具

堆分析工具(HAT)幫助調試無意的對象保持。這個術語用于描述一個對象不再被需要,但是由于來自一個存活對象的通過某個路徑的引用,它卻仍保持存活。HAT提供了一個方便的方式來瀏覽通過HPROF生成的堆快照中的對象拓撲。該工具運行允許一些查詢,包括“顯示從rootset到這個對象的全部引用路徑”。HAT文檔的鏈接見第9節。

8、垃圾回收相關的關鍵選項

許多命令行選項可以用來選擇垃圾回收器、指定堆或代的大小、修改垃圾回收行為、獲取垃圾回收統計信息。本節展示了一些最常用的選項。有關可用選項的更完整的列表和詳細信息,請參閱第9節。注意:你指定的數字可以以“m”或“M”結束表示MB、“k”或“K”結束表示KB、“g”或“G”結束表示GB。

垃圾回收器選擇

選項 垃圾回收選擇
–XX:+UseSerialGC 串行
–XX:+UseParallelGC 并行
–XX:+UseParallelOldGC 并行壓縮
–XX:+UseConcMarkSweepGC 并發標記清除(CMS)

垃圾回收器統計信息

選項 描述
–XX:+PrintGC 在每次垃圾回收時輸出基本信息
–XX:+PrintGCDetails 在每次垃圾回收時,輸出更詳細的信息
–XX:+PrintGCTimeStamps 在每次垃圾回收事件開始時輸出一個時間戳。當每次垃圾回收開始時,與–XX:+PrintGC或–XX:+PrintGCDetails配合使用來顯示。

堆和代的大小

選項 默認值 描述
–Xms<span>n</span> 見第5節 堆的初始字節大小
–Xmx<span>n</span> 見第5節 堆的最大字節大小
–XX:MinHeapFreeRatio=minimum和–XX:MaxHeapFreeRatio=maximum 最小值為40,最大值為70 空閑空間占總堆大小比例的目標范圍。它們被應用到每個代。例如,如果最小值為30,一個代的空閑空間百分比低于30%,該代的大小被擴展以獲得30%的空閑空間。類似的,如果最大值為60,并且空閑空間的百分比超過60%,該代的大小被縮小到只有60%的空閑空間。
–XX:NewSize=n 依賴于平臺 年輕代默認的初始字節大小
–XX:NewRatio=n 在客戶端JVM上為2,在服務器JVM上為8 年輕代和年老代之間的比例。例如,如果n是3,則比例為1:3,伊甸區和幸存空間的組合大小是年輕代和年老代總大小的1/4。
–XX:SurvivorRatio=n 32 每個幸存空間與伊甸區的比例。例如,如果n是7,每個幸存空間是年輕代的1/9(不是1/8,因為有兩個幸存空間)。
–XX:MaxPermSize=n 依賴于平臺 永久代的最大值

并行和并行壓縮回收器的選項

選項 默認值 描述
–XX:ParallelGCThreads=n CPU數量 垃圾回收線程數量
–XX:MaxGCPauseMillis=n 指示回收器期望的暫停時間為n毫秒或者更少。
–XX:GCTimeRatio=n 99 該數值設置了一個目標,總時間的1/(1+n)用于垃圾回收。

CMS回收器選項

選項 默認值 描述
–XX:+CMSIncrementalMode 不啟用 啟用并發階段增量執行的模式,該模式下周期性的停止并發階段以將處理器返回到應用程序。
–XX:+CMSIncrementalPacing 不啟用 允許基于應用程序行為自動控制CMS回收器在放棄處理器之前所做的工作。
–XX:ParallelGCThreads=n CPU數量 用于并行年輕代回收和年老代回收并行部分的垃圾回收線程數量。

9、更多信息

HotSpot垃圾回收和性能調優

人類工程學

選項

工具和故障排除

終結(Finalization)

其它

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 第一章 簡介 J2SE平臺的一大優勢是它的自動化內存管理,避免了開發者去面對內存管理的復雜性。 本文以Sun J2...
    tianyiliusha閱讀 1,025評論 0 1
  • Java 虛擬機有自己完善的硬件架構, 如處理器、堆棧、寄存器等,還具有相應的指令系統。JVM 屏蔽了與具體操作系...
    尹小凱閱讀 1,706評論 0 10
  • 1.一些概念 1.1.數據類型 Java虛擬機中,數據類型可以分為兩類:基本類型和引用類型。基本類型的變量保存原始...
    落落落落大大方方閱讀 4,576評論 4 86
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,367評論 11 349
  • 6.(七)-典型配置舉例1以下配置主要針對分代垃圾回收算法而言。 堆大小設置年輕代的設置很關鍵JVM中最大堆大小有...
    壹點零閱讀 812評論 0 0