原文連結: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即使在最大的可用計算機系統上也具有很高的可擴充性。
JVM的主要元件包括類加載器,運作時資料區和執行引擎。
Hotspot的關鍵元件
下圖高亮顯示了與性能相關的JVM的關鍵元件。
在調整性能時,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)都會把堆構造成三個區域:新生代,老年代,永久代。
所有的對象都在在其中一個塊裡死亡。
而G1收集器采用一個不一樣的方式來劃分堆記憶體。
堆被分割成一組相等大小的堆區域,每個區域都是連續的虛拟記憶體範圍。 每個區域被配置設定成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個部分。
新生代被拆成Eden和兩個suvivor區域。老年代是一個連續的空間。一般情況下不會進行對象整理(譯者:整理記憶體碎片),除非是進行一次Full GC。
2. Young GC怎麼工作
新生代被标記成綠色,老年代是藍色(譯者:希望你不是藍綠色盲)。如果你的應用程式已經運作了一段時間之後,你的虛拟機記憶體看起來應該是這個樣子。在老年代,記憶體是很分散的。
使用CMS時,老年代的對象會在适當的時候被回收掉,再次強調,除非進行一次Full GC,否則不會整理活着的對象的。
3. 新生代收集
活着的對象會從Eden區和suvivor被複制到另一個suvivor區。如果對象的年齡已經達到了門檻值,就會晉升到老年代。
4. Young GC之後
在一次Young GC之後,Eden區和其中一個suvivor會被清空。
圖中,深藍色的是剛剛從新生代晉升到老年代的對象。新生代中綠色的對象是還沒有達到晉升條件的對象(譯者:突然感覺我們就是一個個對象,如果沒有被回收,熬啊熬,就會晉升,哈哈~)。
5. CMS老年代收集
有兩個階段會Stop the World:初始标記,重新标記。當老年代的對象空間占用量達到一個門檻值,CMS就拉開帷幕了。
(1)初始标記會有一個短暫的停頓,用來标記可達對象。(2)并發标記階段是在應用程式執行時,并發的标記活着的對象。然後是(3)重新标記,找到(2)階段遺漏的活着的對象。
6. 老年代收集——并發清除
釋放掉之前幾個階段都沒有标記的對象,不會整理記憶體。
注意:未标記對象 == 死對象
7. 老年代收集——清除之後
在階段(4)收集之後,你可以看到很多對象都被釋放了。你也可以注意到記憶體碎片現象還是存在。(譯者:我實在是受不了了,這句話已經出現幾萬次了)
然後CMS完成(5)重置工作,等待着下一次GC的到來。
5. 一步一步走近G1
G1收集器配置設定堆記憶體和以往的不一樣了。
1. G1堆結構
堆記憶體是一個被拆分成很多固定大小的記憶體區域。
每個區域的大小是JVM啟動時決定的。JVM通常會化成出2000個區域,每個區域大小是1 ~ 32Mb。
2. G1 記憶體配置設定
每個小的區域代表Eden,suvivor或者old。
圖檔上的顔色展現了,每個區域代表的意義。收集時,會把活的對象從一個區域轉移到另一個區域。每個區域可以并行(Stop the World)或者不并行的收集。
每個小的區域可以代表Eden,suvivor或者old。除此之外,還有第四種類型的區域,用來存儲大對象。一般是大小超過單個區域50%的對象會被配置設定到第四種區域裡。這第四種區域是連續的很多個區域的集合。第四種區域就是我們看到的未配置設定的區域。
注意:在寫這篇文章的時候,大對象收集還沒有最優化,是以,建議盡量避免這種大對象的配置設定。
3. G1 中的新生代
堆記憶體被拆分成2000個小區域,大小最小是1Mb,最大是32Mb。藍色代表老年代,綠色代表新生代。
注意:不需要和以前的收集器一樣,把新生代,來年代配置設定在連續的記憶體上,在G1下,是新生代和老年代是可以分散的。
4. G1 中的 Young GC
活着的對象會被轉移(複制/移動)到另一個或多個suvivor區域。如果年齡到了門檻值,就會被配置設定到Old區域。
這個過程是Stop the World的。這個過程會統計很多資訊,比如Eden大小,suvivor大小,還有這次收集的停頓時間等等,這是為了下一次收集做準備。
這種方式,可以很容易的resize(重新定義大小)各個區域的大小。
5. G1 的 Young GC之後
活着的對象被轉移到其他suvivor或者old區域了。
總結一下,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,把活着的對象複制移動到新的未使用的區域。如果隻疏散了新生代,那麼日志是 ,如果新生代和老年代都疏散了,日志記為 |
我們大約了解了各個階段的定義,現在我們來仔細看看每一步究竟是幹什麼的。
6. 初始标記階段
初始标記是搭着Young GC的順風車一起執行的,看GC日志的話,是
GC pause (young)(inital-mark)
。
7. 并發标記階段
如果有空區域(标記為”X”,也就是裡面的對象都死了)被發現,那麼就在重新标記階段直接移除。同樣的,這些資訊也會被統計,用來優化下一次GC。
8. 重新标記階段
空區域會被直接移除回收。并且計算出所有區域的對象的活躍度(liveness)。
9. 複制/清除階段
G1收集器會選擇對象活躍度最低的區域進行收集。新生代和老年代同時被回收。這種情況下,GC日志是
GC pause (mixed)
。這樣,新生代和老年代同僚被回收了。
10. 複制/清除階段之後
選中的區域被回收,并壓縮之後,就是圖中深藍色的和深綠色的。
總結老年代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= -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都必須重新生成。
- 所有這些操作代價都是很大的。
如何避免轉移失敗?
- 增大堆記憶體。
- 增大
,預設是10.-XX:G1ReservePercent=n
- G1使用一個保留的記憶體,建立出一個假的記憶體上限,當記憶體失敗時,就會使用這個保留的記憶體。(譯者:凡事留一線,日後好相見)
- 增大
- 更早的執行GC。
- 使用
來增加GC的執行線程。-XX:ConcGCThreads=n
完整的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 |