1. 程式人生 > >【淺度渣文】JVM——G1收集器

【淺度渣文】JVM——G1收集器

原文連結:http://www.dubby.cn/detail.html?id=9059

1. 概述

硬體和軟體要求

  • 作業系統要求Windows XP或者更高,Mac OS X和Linux都可以。請注意,這些測試操作是在Windows 7上完成的,尚未在所有平臺上進行測試。 但是,一切都應該在OS X或Linux上正常工作。 當然,你的機器有一個以上的核心就更好了。
  • Java 7 Update 9或者更高版本。
  • 最新的Java 7 Demos和示例Zip。

準備內容

  • 安裝好Java 7u9 或者更高版本。
  • 從官網下載下來示例程式碼,解壓後,比如放在C:\javademos下。

2. Java和JVM

Java預覽

Java是Sun Microsystems在1995年首次釋出的程式語言和計算平臺。它是支援Java程式(包括通用工具,遊戲和商業應用程式)的基礎技術。 Java執行在全世界超過8.5億臺個人計算機上,並在全球數十億臺裝置上執行,包括移動和電視裝置。 Java由許多關鍵元件組成,總體而言,它們共同組成了Java平臺。

Java執行時版本

當你下載Java時,你就已經獲得了Java執行時環境(JRE)。JRE是由Java虛擬機器(JVM),Java核心類庫和輔助性的Java類庫組成。如果你想要在你的電腦上執行Java程式,那麼這三個組成部分都是需要安裝好的。使用Java 7的時候,你可以在作業系統上執行Java應用程式,或者使用Java Web Start從Web上安裝然後執行Java應用程式,或者作為一個Web嵌入式應用程式執行在瀏覽器裡(JavaFX)。

Java程式語言

Java是一個面向物件的程式語言,有下面這些特性。

  • 平臺無關性——Java應用被編譯成位元組碼,儲存在class檔案裡,然後被JVM載入。由於Java應用是執行在JVM中,而不是直接執行在作業系統上,所以他們可以執行在各個作業系統上。(譯者:也就是一次編寫,到處執行,JVM幫我做了平臺相容,當然,不可能真的平臺無關性)
  • 面向物件——Java吸收了C和C++的很多特性,並做了一些優化。
  • 自動垃圾回收——Java會自動分配和釋放記憶體,程式設計師不會有負擔。(譯者:但是多了了解GC機制的負擔,不然你也不會看這篇文章了)
  • 豐富的標準庫——Java擁有很多預先設計好的類,我們可以直接用,比如:輸入輸出,網路,日期等等。

JDK

Java開發工具包(JDK)是開發Java應用所需的一系列的工具包。有了JDK,你可以編譯你用Java寫的程式,並且執行。除此之外,JDK還提供了打包和分發應用程式的工具。

JDK和JRE公用了Java應用程式介面(Java API)。Java API是預先打包好的類庫,開發者可以直接使用。Java API讓開發者的開發工作變得更簡單,比如:string的處理,時間的處理,網路,各種資料結構的集合(例如:lists, maps, stacks, and queues)。

JVM

Java虛擬機器(JVM)是一個抽象的計算機。 JVM是一個看起來像一個計算機的程式,可以執行寫入到JVM中的程式。 這樣,Java程式就被寫入到同一組介面和庫中。 針對特定作業系統的每個JVM實現,將Java程式設計指令轉換為在本地作業系統上執行的指令和命令。 這樣,Java程式就實現了平臺獨立性。

Sun公司完成的Java虛擬機器的第一個原型實現,模擬了由類似當代個人數字助理的手持裝置託管的軟體中的Java虛擬機器指令集。 Oracle的當前虛擬機器實現了移動端,桌面和伺服器裝置上的Java虛擬機器。但Java虛擬機器不承擔任何特定的實現技術,主機硬體或主機作業系統。 它沒有一個固有的解釋,(只是一個規範),你也可以通過將其指令集編譯為矽CPU來實現。 它也可以用微碼或直接用矽來實現。

Java虛擬機器對Java程式語言一無所知,只知道特定的二進位制格式,即類檔案格式class。 類檔案包含Java虛擬機器指令(或位元組碼)和符號表以及其他輔助資訊。

為了安全起見,Java虛擬機器對類檔案中的程式碼施加了強烈的語法和結構限制。 但是,Java虛擬機器可以託管任何具有可以用有效的類檔案表示的功能的語言。正因如此,很多其他語言的實現者,為了享受JVM帶來的遍歷,他們可以把自己的語言編譯成class檔案交給JVM來執行。

探索JVM架構

Hotspot的架構

HotSpot JVM擁有支援強大功能和功能的基礎架構,並支援實現高效能和大規模可擴充套件性的能力。 例如,HotSpot JVM JIT編譯器會生成動態優化。 換句話說,他們在Java應用程式執行時做出優化決策,並生成針對底層系統體系結構的高效能本地機器指令。 此外,通過其執行時環境和多執行緒垃圾收集器的成熟發展和持續工程,HotSpot JVM即使在最大的可用計算機系統上也具有很高的可擴充套件性。

image

JVM的主要元件包括類載入器,執行時資料區和執行引擎。

Hotspot的關鍵元件

下圖高亮顯示了與效能相關的JVM的關鍵元件。

image

在調整效能時,JVM有三個重點關注的元件。 堆是你的物件資料儲存的地方。 這個區域由啟動時選擇的垃圾收集器管理。 大多數調優選項都是針對堆的大小以及為您的情況選擇最合適的垃圾收集器。 JIT編譯器對效能也有很大的影響,但很少需要使用較新版本的JVM進行調優。

效能基礎

通常,在調整Java應用程式時,重點是兩個主要目標之一:響應性或吞吐量。 隨著教程的進展,我們將回顧這些概念。

響應性

響應性指的是一個應用程式或者一個系統可以多快的響應一個請求。舉個例子:

  • 桌面應用響應UI事件(點選,滑動等)的速度。
  • 一個網站返回頁面的速度。
  • 資料庫查詢結果返回的速度。

對於一個關注響應性的應用,是不能接受長時間停頓的。優化的目標一般是加快響應速度。

吞吐量

吞吐量關注的是在一定時間內,應用程式或系統可以完成的工作量。舉個例子:

  • 給定時間,完成的事物數量。
  • 一個小時內,一個批處理可以完成的job數量。
  • 一個小時內,資料庫可以完成的查詢量。

長時間的停頓對於關注吞吐量的應用來說,是可以接受的。因為關注的是一個更長時間的的工作效率,而不是儘快結束一個請求。

3. G1收集器

G1收集器(Garbage-First Collector)是一個適合服務端,多處理器,大記憶體的場景。G1收集器可以很大概率的滿足預期的停頓時間,同時實現高吞吐。G1收集器在JDK 7 update 4之後就已經支援了。G1收集器設計主要用於以下應用:

  • 可以與CMS收集器等應用程式執行緒同時執行。
  • 在較短的停頓時間內,完成空閒記憶體碎片的整理。
  • 需要更可預測的GC暫停持續時間。
  • 不想犧牲過多的吞吐量。
  • 不需要更大的Java堆(譯者:可參考複製演算法)。

G1計劃作為併發商標掃描收集器(CMS)的長期替代品。 比較G1和CMS,有一些差異使得G1成為更好的解決方案。 一個區別是G1是一個壓縮演算法的實現。 G1充分壓縮空間以避免使用細粒度的自由列表進行分配,而是依賴於區域。 這大大簡化了收集器的實現,並且大部分消除了潛在的碎片問題。 此外,G1提供比CMS收集器更多的可預測的垃圾收集暫停,並允許使用者指定所需的暫停目標。

G1概述

之前的垃圾收集器(serial, parallel, CMS)都會把堆構造成三個區域:新生代,老年代,永久代。

image

所有的物件都在在其中一個塊裡死亡。

而G1收集器採用一個不一樣的方式來劃分堆記憶體。

image

堆被分割成一組相等大小的堆區域,每個區域都是連續的虛擬記憶體範圍。 每個區域被分配成eden, survivor或者old,但是他們沒有固定的大小。 這提供了更大的記憶體使用靈活性。

在執行垃圾收集時,G1的執行方式類似於CMS收集器。 G1執行一個併發的全域性標記階段來確定整個堆中物件的活性。 標記階段完成後,G1知道哪些區域大部分是空的。 它首先收集這些地區,這往往產生大量的自由空間。 這就是為什麼這種垃圾收集方法稱為垃圾優先。 顧名思義,G1將其收集和壓縮活動集中在可能充滿可回收物件的堆的區域,即垃圾。 G1使用暫停預測模型來滿足使用者定義的暫停時間目標,並基於指定的暫停時間目標選擇要收集的區域的數量。

由G1標記的回收時機成熟的區域就是要被回收的垃圾。 G1將物件從堆的一個或多個區域複製到堆上的單個區域,並在此過程中壓縮並釋放記憶體。 這種撤離在多處理器上並行執行,以減少暫停時間並提高吞吐量。 因此,對於每個垃圾收集,G1不斷地減少碎片,在使用者定義的暫停時間內工作。 這超出了以前的兩種方法的能力。 CMS(併發標記掃描)垃圾收集器不會執行壓縮。 ParallelOld垃圾收集只執行全堆壓縮,導致相當多的暫停時間。

請注意,G1不是實時收集器。 它以高概率滿足設定的暫停時間目標,但不是絕對確定的。 根據以前收集的資料,G1會估算在使用者指定的目標時間內可以收集多少個區域。 因此,收集者具有相當準確的收集區域成本的模型,並且使用該模型來確定在停留時間目標內停留時收集哪些區域和收集多少區域。

注意:G1具有併發(與應用程式執行緒一起執行,例如細化,標記,清除)和並行(多執行緒,例如stop the world)階段。 Full GC仍然是單執行緒的,但是如果調整得當,應用程式應該可以避免Full GC。

G1 的記憶體佔用

如果你是從ParallelOldGC或者CMS遷移到G1的話,你會發現,你似乎擁有了一個更大記憶體。這主要與“統計”資料結構有關,例如Remembered Sets和Collection Sets。

Remembered Sets或者RSets追蹤物件應用在哪裡區域裡。每個堆的區域都有一個TSet。RSet可以並行的,獨立的手機一個區域的物件引用。RSets的記憶體佔用少於5%。

Collection Sets或者CSets將會在一個GC中被回收。所有活著的物件會被疏散(copied/moved)。CSets可以是Eden, survivor和old generation。CSets對記憶體的佔用少於1%。

推薦使用G1的場景

G1的第一個關注點就是為執行應用程式的使用者提供一個解決方案,這些應用程式需要能保證有限GC延遲,並且是個大堆。 這意味著堆大小約6GB或更大,穩定可預測的暫停時間低於0.5秒。

現在使用CMS或者ParallelOldGC垃圾收集器執行的應用程式如果應用程式具有以下一個或多個特性,將有益於切換到G1。

  • Full GC持續時間太長或太頻繁。
  • 物件分配率或提升率明顯不同。
  • 不想要長時間GC停頓(超過0.5到1second)

注意:如果你使用的是CMS或者ParallelOldGC,並且你的應用也沒有經歷過長時間的GC停頓,你完全可以保持不變(譯者:不需要為了用G1還來折騰自己,何必呢)。就算不使用G1收集器,你依然可以使用最新的JDK。

4. 複習CMS收集器

回顧分代GC和CMS

併發標記掃描(CMS)收集器(也稱為併發低暫停收集器)收集終身代。 它試圖通過與應用程式執行緒同時執行大部分垃圾收集工作來儘量減少由於垃圾收集造成的暫停。 通常情況下,併發的低暫停收集器不會複製或壓縮活動物件。 垃圾收集完成時不移動活動物件。 如果碎片成為問題,請分配一個更大的堆。

注意:年輕一代的CMS收集器使用與並行收集器相同的演算法。

CMS的收集階段

CMS在收集老年代時,會執行下面的步驟:

階段 描述
1、初始化標記(Stop the World) 老一代的物件被“標記”為可達,包括年輕一代可能到達的物件。停頓時間一般較短。
2、併發標記 在應用程式執行緒執行時,併發的遍歷老年代物件,生成可達物件的物件圖。這個可達性分析在階段2,3,5都會執行,並且被掃描到的物件都會被立即標記成活著。
3、重新標記(Stop the World) 查詢併發標記階段錯過的物件,也就是在收集器完成了對物件的跟蹤後,然後Java應用程式執行緒更新的物件。
4、併發清除 收集那些在標記階段已經被標記為不可達的物件。死亡物件會被新增到Free List中,以供後續分配使用。在這個時候可能會對死物件進行合併。注意,不會移動活著的物件。
5、重置 清空這一次收集的統計資訊,為下次收集做準備。

複習垃圾回收的步驟

1. CMS的堆結構

堆被拆成3個部分。

image

新生代被拆成Eden和兩個suvivor區域。老年代是一個連續的空間。一般情況下不會進行物件整理(譯者:整理記憶體碎片),除非是進行一次Full GC。

2. Young GC怎麼工作

新生代被標記成綠色,老年代是藍色(譯者:希望你不是藍綠色盲)。如果你的應用程式已經運行了一段時間之後,你的虛擬機器記憶體看起來應該是這個樣子。在老年代,記憶體是很分散的。

image

使用CMS時,老年代的物件會在適當的時候被回收掉,再次強調,除非進行一次Full GC,否則不會整理活著的物件的。

3. 新生代收集

活著的物件會從Eden區和suvivor被複制到另一個suvivor區。如果物件的年齡已經達到了閾值,就會晉升到老年代。

image

4. Young GC之後

在一次Young GC之後,Eden區和其中一個suvivor會被清空。

image

圖中,深藍色的是剛剛從新生代晉升到老年代的物件。新生代中綠色的物件是還沒有達到晉升條件的物件(譯者:突然感覺我們就是一個個物件,如果沒有被回收,熬啊熬,就會晉升,哈哈~)。

5. CMS老年代收集

有兩個階段會Stop the World:初始標記,重新標記。當老年代的物件空間佔用量達到一個閾值,CMS就拉開帷幕了。

image

(1)初始標記會有一個短暫的停頓,用來標記可達物件。(2)併發標記階段是在應用程式執行時,併發的標記活著的物件。然後是(3)重新標記,找到(2)階段遺漏的活著的物件。

6. 老年代收集——併發清除

釋放掉之前幾個階段都沒有標記的物件,不會整理記憶體。

image

注意:未標記物件 == 死物件

7. 老年代收集——清除之後

在階段(4)收集之後,你可以看到很多物件都被釋放了。你也可以注意到記憶體碎片現象還是存在。(譯者:我實在是受不了了,這句話已經出現幾萬次了)

image

然後CMS完成(5)重置工作,等待著下一次GC的到來。

5. 一步一步走近G1

G1收集器分配堆記憶體和以往的不一樣了。

1. G1堆結構

堆記憶體是一個被拆分成很多固定大小的記憶體區域。

image

每個區域的大小是JVM啟動時決定的。JVM通常會化成出2000個區域,每個區域大小是1 ~ 32Mb。

2. G1 記憶體分配

每個小的區域代表Eden,suvivor或者old。

image

圖片上的顏色展現了,每個區域代表的意義。收集時,會把活的物件從一個區域轉移到另一個區域。每個區域可以並行(Stop the World)或者不併行的收集。

每個小的區域可以代表Eden,suvivor或者old。除此之外,還有第四種類型的區域,用來儲存大物件。一般是大小超過單個區域50%的物件會被分配到第四種區域裡。這第四種區域是連續的很多個區域的集合。第四種區域就是我們看到的未分配的區域。

注意:在寫這篇文章的時候,大物件收集還沒有最優化,所以,建議儘量避免這種大物件的分配。

3. G1 中的新生代

堆記憶體被拆分成2000個小區域,大小最小是1Mb,最大是32Mb。藍色代表老年代,綠色代表新生代。

image

注意:不需要和以前的收集器一樣,把新生代,來年代分配在連續的記憶體上,在G1下,是新生代和老年代是可以分散的。

4. G1 中的 Young GC

活著的物件會被轉移(複製/移動)到另一個或多個suvivor區域。如果年齡到了閾值,就會被分配到Old區域。

image

這個過程是Stop the World的。這個過程會統計很多資訊,比如Eden大小,suvivor大小,還有這次收集的停頓時間等等,這是為了下一次收集做準備。

這種方式,可以很容易的resize(重新定義大小)各個區域的大小。

5. G1 的 Young GC之後

活著的物件被轉移到其他suvivor或者old區域了。

image

總結一下,G1的Young GC的特點:

  • 堆被拆分成多個區域。
  • 新生代有一些並不連續的區域組成。這樣可以很容易的擴容或收縮新生代的大小。
  • Young GC會Stop the World。
  • Young GC是多執行緒並行的。
  • 活著的物件會被複制移動到suvior或者old區域。

G1的老年代收集

和CMS一樣,G1也是被設計成一款低停頓的GC收集器。下面的表格描述了G1的老年代收集階段。

G1收集階段 —— 併發標記迴圈階段

G1的老年代收集步驟如下,請注意,其中有一些步驟是Young GC的一部分。

階段 描述
1、初始標記(Stop the World) 這會Stop the World。他會搭著Young GC的順風車,順便標記那些新生代(根區域/root regions)可以引用到的老年代中的物件。
2、根區域掃描 掃描新生代,找到老年代中哪些物件被新生代中的物件引用。這個階段不會中斷應用程式的執行。這個階段必須在Young GC發生之前完成。
3、併發標記 找到整個堆中的活著的物件。這個和應用程式併發執行。但是,這個階段是可能被Young GC中斷的。
4、重新標記(Stop the World) 完成活物件的標記。使用SATB演算法(snapshot-at-the-beginning)(這個演算法比CMS使用的演算法快很多)
5、清除(Stop the World也是併發) 1.統計活物件和完全空閒的區域(Stop the World);2.清空RSets(Stop the World);3.重置空閒區域,並且回收到Free List上(併發)
*、複製(Stop the World) Stop the World,把活著的物件複製移動到新的未使用的區域。如果只疏散了新生代,那麼日誌是GC pause (young),如果新生代和老年代都疏散了,日誌記為GC Pause (mixed)

我們大約瞭解了各個階段的定義,現在我們來仔細看看每一步究竟是幹什麼的。

6. 初始標記階段

初始標記是搭著Young GC的順風車一起執行的,看GC日誌的話,是GC pause (young)(inital-mark)

image

7. 併發標記階段

如果有空區域(標記為"X",也就是裡面的物件都死了)被發現,那麼就在重新標記階段直接移除。同樣的,這些資訊也會被統計,用來優化下一次GC。

image

8. 重新標記階段

空區域會被直接移除回收。並且計算出所有區域的物件的活躍度(liveness)。

image

9. 複製/清除階段

G1收集器會選擇物件活躍度最低的區域進行收集。新生代和老年代同時被回收。這種情況下,GC日誌是GC pause (mixed)。這樣,新生代和老年代同事被回收了。

image

10. 複製/清除階段之後

選中的區域被回收,並壓縮之後,就是圖中深藍色的和深綠色的。

image

總結老年代GC

G1的老年代GC的特點是:

  • 併發標記階段
    • 在應用程式執行時,併發的計算出各個區域的活躍度。
    • 根據活躍度判斷出哪些區域是最值得回收的。
    • 沒有類似CMS的清除階段。
  • 重新標記階段
    • 使用Snapshot-at-the-Beginning (SATB) 演算法,這個演算法比CMS的演算法更高效。
    • 完全空的區域會被回收。
  • 複製/清除階段
    • 新生代和老年代同時被回收。
    • 老年代的選擇是根據活躍度來確定的。

6. 命令列選項和最佳實踐

基本的命令列

為了使用G1收集器,我們需要使用-XX:+UseG1GC

這裡我們用demo來演示(首先你需要進入你demo的目錄demo/jfc/Java2D下),

java -Xmx50m -Xms50m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar  Java2Demo.jar
複製程式碼

主要引數介紹

-XX:+UseG1GC——告訴JVM使用G1收集器。 -XX:MaxGCPauseMillis=200——設定一個最大停頓時間。這是個軟目標,也就是JVM會盡最大的努力去滿足你的目標(譯者:實在滿足不了,你也拿他沒辦法)。因此,有時候可能無法達到你的要求。預設值是200ms。 XX:InitiatingHeapOccupancyPercent=45——堆總量使用比例到達這個值時,開始一趟GC。是堆總量的佔用比例,而不是某一個代的佔用比例。0代表一直迴圈執行GC,預設值是45。

最佳實現

這裡有一些使用G1的最佳實踐的建議。

不要設定新生代的容量

如果你使用-Xmn來指定新生代大小,干預G1的行為(譯者:G1就會很生氣,後果很嚴重)。

  • G1將不會遵從你預期的停頓時間,也就是說,這個選項會關閉-XX:MaxGCPauseMillis
  • G1將不能動態的擴充套件和收縮你的新生代,因為已經指定了。

使用響應時間來作為標準

不要使用平均響應時間來設定XX:MaxGCPauseMillis=<N>,考慮使用你期望的響應時間的90%甚至更高的值來設定。也就是說90%的使用者(客戶端/?)請求響應時間不會超過預設的目標值。因為,這個值只是一個目標值,並不能精確保證滿足。

轉移失敗?

對 survivors 或 promoted objects 進行GC時如果JVM的heap區不足就會發生晉升失敗(promotion failure)。堆記憶體不能繼續擴充,因為已經達到最大值了。可以使用-XX:+PrintGCDetails,這樣在轉移失敗時,會列印to-space overflow。這種操作很昂貴!

  • GC任然要繼續,所以空間必須被釋放。
  • 拷貝失敗的物件必須放到合適的地方。
  • CSet區域中任何更新過的RSets都必須重新生成。
  • 所有這些操作代價都是很大的。

如何避免轉移失敗?

  • 增大堆記憶體。
    • 增大-XX:G1ReservePercent=n,預設是10.
    • G1使用一個保留的記憶體,創建出一個假的記憶體上限,當記憶體失敗時,就會使用這個保留的記憶體。(譯者:凡事留一線,日後好相見)
  • 更早的執行GC。
  • 使用-XX:ConcGCThreads=n來增加GC的執行執行緒。

完整的G1命令列選項

下面給出G1的完整命令列選項,使用時,請記住上面的最佳實踐。

選項和預設值 描述
-XX:+UseG1GC 使用G1收集器
-XX:MaxGCPauseMillis=n 設定一個預期的停頓時間,記住這只是個軟目的,JVM會盡力去實現
-XX:InitiatingHeapOccupancyPercent=n 啟動併發GC週期時的堆記憶體佔用百分比. G1之類的垃圾收集器用它來觸發併發GC週期,基於整個堆的使用率,而不只是某一代記憶體的使用比. 值為 0 則表示"一直執行GC迴圈". 預設值為 45.
-XX:NewRatio=n 新生代和老年代的大小比例(new/old),預設是2
-XX:SurvivorRatio=n eden/suvivor的比例,預設是8
-XX:MaxTenuringThreshold=n 物件晉升的年齡,預設是15
-XX:ParallelGCThreads=n 收集器併發階段使用的執行緒數。預設值是取決於JVM執行的平臺
-XX:ConcGCThreads=n 設定收集器的執行緒數。預設值是取決於JVM執行的平臺
-XX:G1ReservePercent=n 設定G1保留記憶體,防止轉移失敗
-XX:G1HeapRegionSize=n G1收集器把堆記憶體細分成很多個大小一致的小區域。這個選項是設定每個區域的大小預設值是根據堆的總量,算出的。範圍是1 Mb ~ 32 Mb