JVM性能監測及調優(3)

如何優化垃圾回收機制?

我們知道,在 Java 開發中,開發人員是無需過度關注對象的回收與釋放的,JVM 的垃圾回收機制可以減輕不少工作量。但完全交由 JVM 回收對象,也會增加回收性能的不確定性。在一些特殊的業務場景下,不合適的垃圾回收算法以及策略,都有可能導致系統性能下降。

面對不同的業務場景,垃圾回收的調優策略也不一樣。例如,在對內存要求苛刻的情況下,需要提高對象的回收效率;在 CPU 使用率高的情況下,需要降低高併發時垃圾回收的頻率。可以說,垃圾回收的調優是一項必備技能。

我們就把這項技能的學習進行拆分,看看回收(後面簡稱 GC)的算法有哪些,體現 GC 算法好壞的指標有哪些,又如何根據自己的業務場景對 GC 策略進行調優?

垃圾回收機制
掌握 GC 算法之前,我們需要先弄清楚 3 個問題。第一,回收發生在哪裏?第二,對象在什麼時候可以被回收?第三,如何回收這些對象?

  1. 回收發生在哪裏?
    JVM 的內存區域中,程序計數器、虛擬機棧和本地方法棧這 3 個區域是線程私有的,隨着線程的創建而創建,銷燬而銷燬;棧中的棧幀隨着方法的進入和退出進行入棧和出棧操作,每個棧幀中分配多少內存基本是在類結構確定下來的時候就已知的,因此這三個區域的內存分配和回收都具有確定性。

那麼垃圾回收的重點就是關注堆和方法區中的內存了,堆中的回收主要是對象的回收,方法區的回收主要是廢棄常量和無用的類的回收。

  1. 對象在什麼時候可以被回收?

那 JVM 又是怎樣判斷一個對象是可以被回收的呢?一般一個對象不再被引用,就代表該對象可以被回收。目前有以下兩種算法可以判斷該對象是否可以被回收。

引用計數算法:這種算法是通過一個對象的引用計數器來判斷該對象是否被引用了。每當對象被引用,引用計數器就會加 1;每當引用失效,計數器就會減 1。當對象的引用計數器的值爲 0 時,就說明該對象不再被引用,可以被回收了。這裏強調一點,雖然引用計數算法的實現簡單,判斷效率也很高,但它存在着對象之間相互循環引用的問題。

可達性分析算法:GC Roots 是該算法的基礎,GC Roots 是所有對象的根對象,在 JVM 加載時,會創建一些普通對象引用正常對象。這些對象作爲正常對象的起始點,在垃圾回收時,會從這些 GC Roots 開始向下搜索,當一個對象到 GC Roots 沒有任何引用鏈相連時,就證明此對象是不可用的。目前 HotSpot 虛擬機採用的就是這種算法。

以上兩種算法都是通過引用來判斷對象是否可以被回收。在 JDK 1.2 之後,Java 對引用的概念進行了擴充,將引用分爲了以下四種:

  1. 如何回收這些對象?

瞭解完 Java 程序中對象的回收條件,那麼垃圾回收線程又是如何回收這些對象的呢?JVM 垃圾回收遵循以下兩個特性。

自動性:Java 提供了一個系統級的線程來跟蹤每一塊分配出去的內存空間,當 JVM 處於空閒循環時,垃圾收集器線程會自動檢查每一塊分配出去的內存空間,然後自動回收每一塊空閒的內存塊。

不可預期性:一旦一個對象沒有被引用了,該對象是否立刻被回收呢?答案是不可預期的。我們很難確定一個沒有被引用的對象是不是會被立刻回收掉,因爲有可能當程序結束後,這個對象仍在內存中。

垃圾回收線程在 JVM 中是自動執行的,Java 程序無法強制執行。我們唯一能做的就是通過調用 System.gc 方法來"建議"執行垃圾收集器,但是否可執行,什麼時候執行?仍然不可預期。

GC 算法

JVM 提供了不同的回收算法來實現這一套回收機制,通常垃圾收集器的回收算法可以分爲以下幾種:

如果說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現,JDK1.7 update14 之後 Hotspot 虛擬機所有的回收器整理如下(以下爲服務端垃圾收集器):

其實在 JVM 規範中並沒有明確 GC 的運作方式,各個廠商可以採用不同的方式實現垃圾收集器。我們可以通過 JVM 工具查詢當前 JVM 使用的垃圾收集器類型,首先通過 ps 命令查詢出進程 ID,再通過 jmap -heap ID 查詢出 JVM 的配置信息,其中就包括垃圾收集器的設置類型。

GC 性能衡量指標

一個垃圾收集器在不同場景下表現出的性能也不一樣,那麼如何評價一個垃圾收集器的性能好壞呢?我們可以藉助一些指標。

吞吐量:這裏的吞吐量是指應用程序所花費的時間和系統總運行時間的比值。我們可以按照這個公式來計算 GC 的吞吐量:系統總運行時間 = 應用程序耗時 +GC 耗時。如果系統運行了 100 分鐘,GC 耗時 1 分鐘,則系統吞吐量爲 99%。GC 的吞吐量一般不能低於 95%。

停頓時間:指垃圾收集器正在運行時,應用程序的暫停時間。對於串行回收器而言,停頓時間可能會比較長;而使用併發回收器,由於垃圾收集器和應用程序交替運行,程序的停頓時間就會變短,但其效率很可能不如獨佔垃圾收集器,系統的吞吐量也很可能會降低。

垃圾回收頻率:多久發生一次指垃圾回收呢?通常垃圾回收的頻率越低越好,增大堆內存空間可以有效降低垃圾回收發生的頻率,但同時也意味着堆積的回收對象越多,最終也會增加回收時的停頓時間。所以我們只要適當地增大堆內存空間,保證正常的垃圾回收頻率即可。

查看&分析 GC 日誌

已知了性能衡量指標,現在我們需要通過工具查詢 GC 相關日誌,統計各項指標的信息。首先,我們需要通過 JVM 參數預先設置 GC 日誌,通常有以下幾種 JVM 參數設置:-XX:+PrintGC 輸出GC日誌

-XX:+PrintGC 輸出GC日誌
-XX:+PrintGCDetails 輸出GC的詳細日誌
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進行GC的前後打印出堆的信息
-Xloggc:…/logs/gc.log 日誌文件的輸出路徑

這裏使用如下參數來打印日誌:

-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs

打印後的日誌爲:

上圖是運行很短時間的 GC 日誌,如果是長時間的 GC 日誌,我們很難通過文本形式去查看整體的 GC 性能。此時,我們可以通過GCViewer工具打開日誌文件,圖形化界面查看整體的 GC 性能,如下圖所示:

通過工具,我們可以看到吞吐量、停頓時間以及 GC 的頻率,從而可以非常直觀地瞭解到 GC 的性能情況。

這裏我再推薦一個比較好用的 GC 日誌分析工具,GCeasy是一款非常直觀的 GC 日誌分析工具,我們可以將日誌文件壓縮之後,上傳到 GCeasy 官網即可看到非常清楚的 GC 日誌分析結果:

GC 調優策略
找出問題後,就可以進行調優了,下面介紹幾種常用的 GC 調優策略。

1.降低 Minor GC 頻率
通常情況下,由於新生代空間較小,Eden 區很快被填滿,就會導致頻繁 Minor GC,因此我們可以通過增大新生代空間來降低 Minor GC 的頻率。

可能你會有這樣的疑問,擴容 Eden 區雖然可以減少 Minor GC 的次數,但不會增加單次 Minor GC 的時間嗎?如果單次 Minor GC 的時間增加,那也很難達到我們期待的優化效果呀。

我們知道,單次 Minor GC 時間是由兩部分組成:T1(掃描新生代)和 T2(複製存活對象)。假設一個對象在 Eden 區的存活時間爲 500ms,Minor GC 的時間間隔是 300ms,那麼正常情況下,Minor GC 的時間爲:T1+T2。

當我們增大新生代空間,Minor GC 的時間間隔可能會擴大到 600ms,此時一個存活 500ms 的對象就會在 Eden 區中被回收掉,此時就不存在複製存活對象了,所以再發生 Minor GC 的時間爲:兩次掃描新生代,即 2T1。

可見,擴容後,Minor GC 時增加了 T1,但省去了 T2 的時間。通常在虛擬機中,複製對象的成本要遠高於掃描成本。

如果在堆內存中存在較多的長期存活的對象,此時增加年輕代空間,反而會增加 Minor GC 的時間。如果堆中的短期對象很多,那麼擴容新生代,單次 Minor GC 時間不會顯著增加。因此,單次 Minor GC 時間更多取決於 GC 後存活對象的數量,而非 Eden 區的大小。

  1. 降低 Full GC 的頻率

通常情況下,由於堆內存空間不足或老年代對象太多,會觸發 Full GC,頻繁的 Full GC 會帶來上下文切換,增加系統的性能開銷。我們可以使用哪些方法來降低 Full GC 的頻率呢?

減少創建大對象:在平常的業務場景中,我們習慣一次性從數據庫中查詢出一個大對象用於 web 端顯示。例如,我之前碰到過一個一次性查詢出 60 個字段的業務操作,這種大對象如果超過年輕代最大對象閾值,會被直接創建在老年代;即使被創建在了年輕代,由於年輕代的內存空間有限,通過 Minor GC 之後也會進入到老年代。這種大對象很容易產生較多的 Full GC。

我們可以將這種大對象拆解出來,首次只查詢一些比較重要的字段,如果還需要其它字段輔助查看,再通過第二次查詢顯示剩餘的字段。

增大堆內存空間:在堆內存不足的情況下,增大堆內存空間,且設置初始化堆內存爲最大堆內存,也可以降低 Full GC 的頻率。

選擇合適的 GC 回收器

假設我們有這樣一個需求,要求每次操作的響應時間必須在 500ms 以內。這個時候我們一般會選擇響應速度較快的 GC 回收器,CMS(Concurrent Mark Sweep)回收器和 G1 回收器都是不錯的選擇。

而當我們的需求對系統吞吐量有要求時,就可以選擇 Parallel Scavenge 回收器來提高系統的吞吐量。

總結

垃圾收集器的種類很多,我們可以將其分成兩種類型,一種是響應速度快,一種是吞吐量高。通常情況下,CMS 和 G1 回收器的響應速度快,Parallel Scavenge 回收器的吞吐量高。

在 JDK1.8 環境下,默認使用的是 Parallel Scavenge(年輕代)+Serial Old(老年代)垃圾收集器,你可以通過文中介紹的查詢 JVM 的 GC 默認配置方法進行查看。

通常情況,JVM 是默認垃圾回收優化的,在沒有性能衡量標準的前提下,儘量避免修改 GC 的一些性能配置參數。如果一定要改,那就必須基於大量的測試結果或線上的具體性能來進行調整。

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