1. 程式人生 > >Hotspot優化指南(上)

Hotspot優化指南(上)

  一次偶然,博主在瀏覽docs.oracle.com/javase的時候發現了《Hotspot虛擬機器垃圾收集調優指南》這篇文件。內心百感交集,之前在看完了周志明的《深入理解Java虛擬機器 JVM高階特性與最佳實踐(第二版)》也有比較長篇的學習記錄部落格發表。不過那也是基於JDK7進行編寫的。後續的8、9、10都沒有找到好的關於JVM更加新的好的內容。由於性子懶惰加上本身英語不佳,一直都沒上官網進行相關研究。本篇文章就是博主在研讀這篇文件的時候所記錄,也是基於現如今最流行的JDK8相關內容,歡迎指正。

  在簡介中文章指出垃圾收集器(GC)是一個記憶體管理工具,它通過了以下三點實現了記憶體管理:     1、將物件分配給新生代和晉升老的物件到老年代     2、併發標記階段在老年代找到存活的物件。當Java堆佔用率超過預設閾值時,Java Hotspot虛擬機器會觸發標記階段。     3、通過並行複製壓縮存活的物件來恢復空閒記憶體,   文中通過一個圖展示了隨著處理器的增多不同GC時間下吞吐量的變化(如圖一),從此圖中我們可以分析出的以下結論:

(圖一)

    1、在處理器比較少的情況下(少於10?),設定越高的GC佔用時間百分比便越會導致吞吐量急劇下降。     2、在小型系統上可以忽略的速度問題會成為大型系統上面的瓶頸,選擇合適的垃圾收集器和必要的調優是值得的

  Ergonomics就是JVM哈垃圾收集器調優的過程,比如基於行為的調優,提高應用程式的效能。JVM為垃圾收集器、堆大小和即時編譯器提供平臺相關的預設的配置。當你需要更少的命令列調優的時候,這些選項可以匹配不同型別的應用。另外,可以基於行為動態調整堆的大小以滿足應用程式的特定行為。這一節描述了這些預設選擇和基於行為的調優。

  以下是GC、堆和執行時編譯器的預設選擇說明:
 

    一個被定義為Server-Class所在機器的要求:       1、兩個或以上的處理器       2、2GB或以上的實體記憶體     在Server-Class機器上,一下是預設選項:       1、吞吐量優先垃圾收集器       2、堆初始化大小為實體記憶體的1/64,最大為1GB       3、最大的堆大小範圍為實體記憶體的1/4到1GB       4、Server模式的執行時編譯器     上面的定義適用於除window 32位系統以外的其他所有平臺。預設執行時編譯器詳情可以檢視官方說明

  基於行為的調優:

    對並行收集器來說,JavaSE基於特定行為的基礎上提供了兩個垃圾收集調優引數:最大暫停時間

應用程式的吞吐量。

    最大暫停時間:暫停時間是垃圾收集器停止應用程式並恢復已不再使用的空間的持續時間。最大暫停時間目標的目的是限制這些暫停時間最長的時間。通過命令列 -XX:MaxGCPauseMillis=<nnn>來指定。通過這個引數的設定,垃圾收集器會調整堆的大小和其他相關引數讓暫停時間儘量接近所設定的最大暫停時間。預設情況下,如果沒有最大暫停時間這些調整可能導致頻繁GC從而降低應用程式的吞吐量

    吞吐量:吞吐量是由垃圾收集時間和應用程式時間來衡量的。通過-XX:GCTimeRatio=<nnn>來設定吞吐量的大小。比如 -XX:GCTimeRatio=19表示垃圾收集的時間佔總應用時間的1/20或者5%。JVM為垃圾收集器、堆大小和即時編譯器提供平臺相關的預設的配置。當你需要更少的命令列調優的時候,這些選項可以匹配不同型別的應用。另外,可以基於行為動態調整堆的大小以滿足應用程式的特定行為。這一節描述了這些預設選擇和基於行為的調優。垃圾收集的時間是新生代和老年代GC時間的和。如果吞吐量目標沒有實現的話,為了讓應用程式執行時間在與垃圾收集時間所佔比重增加,JVM會增大新生代和老年代的大小。

    如果達到了吞吐量和最大暫停時間目標,那麼垃圾收集器就會減少堆的大小,直到讓一個目標(總是吞吐量目標)無法滿足。沒有達到的目標就會被解決(放棄?)

    調優策略

      1、不要設定堆的最大值除非你知道你需要一個比預設最大堆大小更大的堆。選擇一個對你的應用來說足夠了的吞吐量目標。       為了實現設定的吞吐量,堆將增大或縮減到一個大小。應用程式行為的改變會導致堆的增長或收縮。例如,如果應用程式開始以更高的速率分配,堆將會增長以保持相同的吞吐量。       2、如果堆增長到最大值且設定吞吐量目標沒有達到,那麼最大堆大小對於吞吐量目標來說太小了。將最大堆大小設定為接近於平臺的總實體記憶體的值,但不會導致應用程式的交換。再次執行應用程式。如果吞吐量目標仍然沒有滿足,那麼應用程式時間的目標對於平臺上可用的記憶體來說太高了。       3、如果能夠滿足吞吐量目標,但是有一些暫停太長,然後選擇一個最大的暫停時間目標。選擇一個最大的暫停時間目標可能意味著您的吞吐量目標將無法滿足,因此選擇對於應用程式來說是可接受的折衷方案。       4、通常情況下,堆的大小會隨著垃圾收集器試圖滿足相互競爭的目標而振盪。即使應用程式已經達到了穩定狀態,這也是正確的。實現吞吐量目標(可能需要更大的堆)的壓力與最大暫停時間和最小記憶體佔用(兩者都可能需要一個小堆)的目標競爭。

  分代收集演算法的依據便是:絕大部分新產生的物件應該要被回收。如下圖二,所展示的物件生命週期的典型分佈。

(圖二)

  注意:如果垃圾收整合為了瓶頸,你很可能不得不定製總的堆大小以及各個代的大小。檢查詳細的垃圾收集器輸出,然後探索單個性能指標對垃圾收集器引數的敏感度

  預設的分代排列圖(除了Parallel Collector 和 G1)如下圖三

(圖三)

  列印GC日誌

    使用命令列:-verbose:gc 列印示例如下

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

    前面兩條顯示的是minor GC,第三條顯示的事major GC。箭頭前後顯示的是垃圾收集前後的活動物件的組合大小。括號裡面是空閒空間大小。最後面的事垃圾收集執行的時間。

    使用命令列:-XX:+PrintGCDetails 列印示例如下

[GC [DefNew: 64575K->959K(64576K), 0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]

    表明垃圾收集器在新生代將64575K的已用容量大小回收至959K容量大小。花費了0.0457646秒。整個堆的使用大小從196016K變成了133633K。

    使用命令列:-XX:+PrintGCTimeStamps 列印示例如下

111.042: [GC 111.042: [DefNew: 8128K->8128K(8128K), 0.0000505 secs]111.042: [Tenured: 18154K->2311K(24576K), 0.1290354 secs] 26282K->2311K(32704K), 0.1293306 secs]

    在前面添加了時間戳。這是一個老年代收集日誌。

  很多引數影響新生代和老年代的大小,下圖四體現了提交空間與虛擬空間的區別。預留空間的大小可以用-Xmx選項指定。

(圖四)

  Total Heap:

    關於堆和預設堆大小的增長和收縮的討論並不適用於並行收集器。然而,控制堆的總大小和幾代的大小的引數確實適用於並行收集器。     影響垃圾收集效能的最重要的因素是可用記憶體總量,因為當各個”代“被填滿產生GC的時候,吞吐量與可用的記憶體數量成反比。

    預設情況下,虛擬機器在每個垃圾收集操作中增加或收縮堆,以試圖將每個垃圾收集操作後的活動物件和空閒空間的比例保持在一個特定範圍內的。這個比例值由-XX:MinHeapFreeRatio=<minimum>-XX:MaxHeapFreeRatio=<maximum>控制。總堆的大小範圍在-Xms-Xmx的之間。

  survivor size:

    在總堆大小設定之後,影響垃圾收集效能的第二大影響因素是用於新生代的堆的比例。新生代越大,minor GC發生的次數就越少。然而,對於一個有限的堆來說,越大的新生代意味著越小的老年代。這將導致頻繁的major GC。最佳的選擇取決於應用程式分配的物件的生命週期分佈。

    預設情況下,新生代大小由-XX:NewRatio=<n>控制。如果n=3,則意味著新生代與老年代的比例為1:3。通過NewSizeMaxNewSize可以設定新生代大小的範圍,對縮小調優粒度來說是非常有效的。

    我們可以通過-XX:SurvivorRatio=<n>來設定 survivor區的大小。例如n=6,則意味著survivor區與Eden區的比例為1:6,因為有兩個survivor區,所以survivor的大小佔新生代總大小的1/8。如果survivor區太小,複製垃圾收集器溢位便會直接將該物件放入老年代。如果survivor太大,他們將毫無用處。虛擬機器會設定一個閥值,物件達到了相應複製(垃圾回收)的次數便會升級到老年代。命令列-XX:+PrintTenuringDistribution 可以用來顯示新生代的閾值和物件的年齡。它對於觀察應用程式的生命週期分佈也很有用。

  這裡討論的是serial collector。Java HotSpot VM包含三種不同型別的收集器,每種型別都具有不同的效能特徵。

    1、序列收集器使用單個執行緒來執行所有垃圾收集工作,這使得它相對高效,因為執行緒之間沒有通訊開銷。它最適合於單處理器機器,因為它不能利用多處理器硬體,儘管它對於具有小資料集的應用程式的多處理器非常有用(高達大約100 MB)。序列收集器在某些硬體和作業系統配置上預設選擇,或者可以顯式地啟用選項-XX:+UseSerialGC

    2、並行收集器(也稱為吞吐量收集器)並行執行minor GC,這可以顯著減少垃圾收集開銷。它適用於在多處理器或多執行緒硬體上執行的中型到大型資料集的應用程式。並行收集器在某些硬體和作業系統配置上預設選擇,也可以通過選項-XX:+UseParallelGC顯式啟用。

    3、大多數併發收集器可以並行的執行大部分工作(例如,當應用程式仍在執行時),以防止垃圾收集暫停。它是為具有中型到大型資料集的應用程式設計的,其中它響應時間比總體吞吐量更重要,因為用於最小化停頓的技術可以降低應用程式的效能。Java HotSpot VM提供了兩個主要併發收集器之間的選擇。使用選項-XX:+UseConcMarkSweepGC啟用CMS收集器或-XX:+UseG1GC啟用G1 收集器。

  除非你的應用程式有相當嚴格的暫停時間需求,首先執行您的應用程式,並允許VM選擇一個收集器。如果有必要,調整堆大小以提高效能。如果效能仍然不能滿足您的目標,那麼使用下面的指導方針作為選擇收集器的依據:

    1、如果你的應用有一個小的資料集(通常小於100MB),那麼你就利用下面的命令列選用序列收集器:-XX:+UseSerialGC.

    2、如果你的應用在一個單處理器上執行,且沒有暫停時間需求,那就讓虛擬機器自動選擇垃圾收集器或者用下面的命令列選用序列收集器:-XX:+UseSerialGC.

    3、如果你的peak應用程式效能是第一優先順序,且沒有暫停時間要求或1秒或更長時間的暫停可以接受,那就讓虛擬機器自動選擇垃圾收集器或者用下面命令列選用並行垃圾收集器:-XX:+UseParallelGC.

    4、如果響應時間比總體吞吐量更重要,垃圾收集暫停必須保持在大約1秒的時間內,那麼就選用併發收集器:-XX:+UseConcMarkSweepGC 或者 -XX:+UseG1GC.

  因為效能依賴於堆的大小、應用程式維護的實時資料量以及可用處理器的數量和速度,上面這些指導原則只提供了選擇收集器的依據。暫停時間對這些因素特別敏感,因此前面提到的1秒的閾值僅是近似的:在許多資料大小和硬體組合上,並行收集器將經歷超過1秒的暫停時間;相反,在某些組合上,併發收集器可能無法保持小於1秒的暫停。

  如果推薦的收集器沒有達到預期的效能,首先嚐試調整堆和生成大小以滿足期望的目標。如果效能仍然不夠,那麼嘗試一個不同的收集器:使用併發收集器來減少暫停時間,並使用並行收集器來提高多處理器硬體的總體吞吐量。