天天看點

深入了解Java的GC原理,掌握JVM 性能調優!

作者:微風01
深入了解Java的GC原理,掌握JVM 性能調優!

對于 Java 開發人員來說,進行程式的性能優化是很有挑戰的工作,也是很有意義的一件事。本篇主要根據 JVM 記憶體模型和垃圾回收的詳細講解,可以更好的了解JVM的調優的根本原理。

JVM記憶體模型

深入了解Java的GC原理,掌握JVM 性能調優!

JVM 架構

  • 類加載器(Classloader):類加載器是JVM的一個子系統,用于加載類檔案。每當我們運作java程式時,它首先由類加載器加載。
  • 類(方法)區(Class(Method) Area):類(方法)區存儲每個類的結構,例如運作時常量池、字段和方法資料、方法的代碼。
  • 堆(Heap):是配置設定對象的運作時資料區域。
  • 堆棧(Stack):Java 堆棧存儲幀。它儲存局部變量和部分結果,并在方法調用和傳回中發揮作用。每個線程都有一個私有的 JVM 堆棧,與該線程同時建立。每次調用方法時都會建立一個新架構。當其方法調用完成時,架構将被銷毀。
  • 程式計數器寄存器(PC):PC(程式計數器)寄存器包含目前正在執行的Java虛拟機指令的位址。
  • 本機方法堆棧(Native Method Stack):它包含應用程式中使用的所有本機方法。
  • 執行引擎(Execution Engine):它包含:一個虛拟處理器;解釋器:讀取位元組碼流然後執行指令。
  • Just-In-Time(JIT)編譯器:它用于提高性能。JIT 同時編譯具有相似功能的位元組碼部分,進而減少編譯所需的時間。這裡,術語“編譯器”是指從Java虛拟機(JVM)的指令集到特定CPU的指令集的翻譯器。
  • Java 本機接口:Java 本機接口 (JNI) 是一個架構,提供與用其他語言(如 C、C++、彙編等)編寫的另一個應用程式進行通信的接口。Java 使用 JNI 架構将輸出發送到控制台或與作業系統互動。

應該已經使用了一些像這樣的 JVM 配置

JAVA_OPTS=”-server -Xms2560m -Xmx2560m -XX:NewSize=1536m -XX:MaxNewSize=1536m -XX:MetaspaceSize=768m -XX:MaxMetaspaceSize=768m -XX:InitialCodeCacheSize=64m -XX:ReservedCodeCacheSize=96m -XX:MaxTenuringThreshold=5″           
  • -server - 啟用“ServerHotspotVM”;該參數在 64 位 JVM 中預設使用。
  • -Xms - 堆的初始空間。
  • -Xmx - 堆的最大空間。
  • -XX:NewSize - 初始新空間。将新大小設定為總堆的一半通常比使用較小的新大小提供更好的性能。
  • -XX:MaxNewSize - 最大新空間。
  • -XX:MetaspaceSize - 靜态内容的初始空間。
  • -XX:MaxMetaspaceSize - 靜态内容的最大空間。
  • -XX:InitialCodeCacheSize - JIT 編譯代碼的初始空間。代碼緩存太小(預設為 48m)會降低性能,因為 JIT 無法優化高頻方法。
  • -XX:ReservedCodeCacheSize - JIT 編譯代碼的最大空間。
  • -XX:MaxTenuringThreshold - 在更新到老年代空間之前,将幸存者保留在幸存者空間中最多 15 次垃圾回收。

那麼 JVM 是如何駐留在記憶體上的?JVM 消耗主機作業系統記憶體上的可用空間。

深入了解Java的GC原理,掌握JVM 性能調優!

然而,在 JVM 内部,存在獨立的記憶體空間(堆、非堆、緩存),以存儲運作時資料和編譯後的代碼。

堆記憶體

  • 堆分為兩部分:Young Generation 和 Old Generation
  • JVM 啟動時配置設定堆(初始大小:-Xms)
  • 應用程式運作時堆大小增加/減少
  • 堆的最大空間:-Xmx
深入了解Java的GC原理,掌握JVM 性能調優!
深入了解Java的GC原理,掌握JVM 性能調優!

以下是有關伺服器應用程式堆大小的一般準則:

  • 除非遇到暫停問題,否則請嘗試為虛拟機授予盡可能多的記憶體。預設大小通常太小。
  • 将-Xms和-Xmx設定為相同的值可以消除虛拟機中最重要的大小調整決策,進而提高可預測性。但是如果設定相同大小出了錯誤,虛拟機将無法進行補償。
  • 一般來說,随着處理器數量的增加,記憶體也會随之增加,因為配置設定可以并行進行。

年輕代(Young Generation)

  • 這是為包含新配置設定的對象而保留的
  • Young Gen 包括三個部分——Eden Memory 和兩個 Survivor Memory 空間(S0、S1)
  • 大多數新建立的對象都會進入Eden space。
  • 當 Eden 空間充滿對象時,将執行 Minor GC(又名 Young Collection),并将所有幸存者對象移動到幸存者空間之一。
  • Minor GC 還會檢查幸存者對象并将它們移動到其他幸存者空間。是以在某一時刻,幸存者的一個空間總是空着的。
  • 經過多次GC後幸存的對象會被移至Old代記憶體空間。通常,這是通過在年輕代對象有資格晉升到老年代之前設定年齡門檻值(-XX:MaxTenuringThreshold)來完成的。

老年代(Old Generation)

  • 這是為包含在多輪 Minor GC 後仍能存活的長壽命對象而保留的
  • 當 Old Gen 空間滿時,将執行 Major GC(又名 Old Collection)(通常需要更長的時間)

非堆記憶體

  • 這包括永久生成(自 Java 8 起被 Metaspace 取代)
  • Perm Gen 存儲每個類的結構,例如運作時常量池、字段和方法資料、方法和構造函數的代碼以及内部字元串
  • 可以使用 -XX:PermSize 和 -XX:MaxPermSize 更改其大小
深入了解Java的GC原理,掌握JVM 性能調優!

高速緩存存儲器

  • 這包括代碼緩存
  • 存儲JIT編譯器生成的編譯代碼(即本機代碼)、JVM内部結構、加載的分析器代理代碼和資料等。
  • 當代碼緩存超過門檻值時,它會被重新整理(GC 不會重新定位對象)。

什麼是GC?

Java 通過一個稱為垃圾收集器的程式提供自動記憶體管理。

“移除不再使用的對象。”

上面的一切都是在堆中完成的,堆是運作時動态記憶體配置設定的空間,用于包含所有 java 對象。除了堆之外,還有堆棧,其中包含支援線程執行的局部變量和函數調用。

Java 垃圾收集的實際工作原理

許多人認為垃圾收集會收集并丢棄死對象。事實上,Java 垃圾收集的作用恰恰相反!活動對象被跟蹤,其他所有對象都被指定為垃圾。這種根本性的誤解可能會導緻許多性能問題。

讓我們從堆開始,它是用于動态配置設定的記憶體區域。在大多數配置中,作業系統會提前配置設定堆,以便在程式運作時由 JVM 管理。這有幾個重要的影響:

  • 對象建立速度更快,因為不需要每個對象都與作業系統進行全局同步。配置設定隻是聲明記憶體數組的某些部分并将偏移指針向前移動(參見圖 2.1)。下一個配置設定從此偏移量開始,并聲明數組的下一部分。
  • 當不再使用某個對象時,垃圾收集器會回收底層記憶體并将其重新用于将來的對象配置設定。這意味着沒有顯式删除,也沒有記憶體傳回給作業系統。
深入了解Java的GC原理,掌握JVM 性能調優!

新對象簡單地配置設定在已用堆的末尾

一旦某個對象不再被引用并且是以應用程式代碼無法通路該對象,垃圾收集器就會将其删除并回收未使用的記憶體。

垃圾收集根——所有對象樹的來源

每個對象樹必須有一個或多個根對象。隻要應用程式可以到達這些根,那麼整棵樹都是可以到達的。但是這些根對象什麼時候被認為是可達的呢?稱為垃圾收集根,它是特殊對象始終是可通路的,任何在其根處具有垃圾收集根的對象也是如此。

Java中有四種GC root:

  • 局部變量通過線程的堆棧保持活動狀态。這不是真實的對象虛拟引用,是以不可見。無論如何,局部變量都是 GC 根。
  • 活動的 Java 線程始終被視為活動對象,是以是 GC 根。這對于線程局部變量尤其重要。
  • 靜态變量由它們的類引用。這一事實使它們成為事實上的 GC 根。類本身可以被垃圾收集,這将删除所有引用的靜态變量。
  • JNI 引用是本機代碼作為 JNI 調用的一部分建立的 Java 對象。這樣建立的對象會被特殊對待,因為 JVM 不知道它是否被本機代碼引用。
深入了解Java的GC原理,掌握JVM 性能調優!

GC 根是 JVM 本身引用的對象,是以可以防止其他所有對象被垃圾收集。

是以,一個簡單的 Java 應用程式具有以下 GC 根:

  • main方法中的局部變量
  • 主線程
  • 主類的靜态變量

标記并清除垃圾

标記可達對象

為了确定哪些對象不再使用,JVM 間歇性地運作所謂的“标記和清除”算法。正如所直覺的,這是一個簡單的兩步過程:

  1. 該算法從 GC 根開始周遊所有對象引用,并将找到的每個對象标記為活動對象。
  2. 所有未被标記對象占用的堆記憶體都會被回收。它隻是被簡單地标記為空閑,基本上清除了未使用的對象。
深入了解Java的GC原理,掌握JVM 性能調優!

活動對象在上圖中表示為藍色。當标記階段結束時,每個活動對象都被标記。是以,所有其他對象(上圖中的灰色資料結構)都無法從 GC 根通路,這意味着應用程式無法再使用無法通路的對象。此類對象被視為垃圾,GC 應在以下階段中删除它們。

标記階段需要注意以下重要方面:

  • 需要停止應用程式線程才能進行标記,因為如果圖表一直在不斷變化,就無法真正周遊圖表。當應用程式線程暫時停止以便 JVM 可以進行内務活動時,這種情況稱為安全點,導緻 Stop The World 暫停。安全點可以因不同的原因而被觸發,但垃圾收集是迄今為止引入安全點的最常見原因。
  • 此暫停的持續時間既不取決于堆中對象的總數,也不取決于堆的大小,而是取決于活動對象的數量。是以增加堆的大小并不會直接影響标記階段的持續時間。
  • 當标記階段完成後,GC就可以進行下一步并開始删除不可達的對象。

删除未使用的對象

對于不同的 GC 算法,未使用對象的删除略有不同,但所有此類 GC 算法都可以分為三步:清除(sweeping)、壓縮(compacting)和複制(copying)。

Sweep

标記和清除算法在概念上使用最簡單的垃圾處理方法,即忽略此類對象。這意味着在标記階段完成後,未通路對象占用的所有空間都被視為空閑,是以可以重用以配置設定新對象。

該方法需要使用所謂的空閑清單記錄每個空閑區域及其大小。空閑清單的管理增加了對象配置設定的開銷。這種方法還有另一個弱點——可能存在大量空閑區域,但如果沒有一個區域足夠大來容納配置設定,配置設定仍然會失敗(在 Java 中會出現 OutOfMemoryError 錯誤)。

它通常被稱為标記-清除算法。

深入了解Java的GC原理,掌握JVM 性能調優!

Compact

Mark-Sweep-Compact算法通過将所有标記的對象 (即活動的對象)移動到記憶體區域的開頭來解決Mark-and-Sweep算法的缺點。這種方法的缺點是增加了GC暫停時間,因為我們需要将所有對象複制到一個新位置,并更新對這些對象的所有引用。Markand Sweep的好處也是顯而易見的--在這樣一個壓縮操作之後,通過指針碰撞,新對象的配置設定再次變得非常便宜。使用這種方法,空閑空間的位置總是已知的,也不會觸發碎片問題。

它通常被稱為标記-壓縮算法。

深入了解Java的GC原理,掌握JVM 性能調優!

Copy

标記和複制算法非常類似于标記和壓縮,因為它們也重新定位所有活動對象。重要的差別在于,對象搬遷的目标是不同的記憶區域,作為幸存對象的新家。标記和複制方法具有一些優點,因為複制可以與标記在同一階段同時發生。缺點是需要多一個記憶體區域,該記憶體區域應該足夠大以容納幸存的對象。

它通常被稱為标記複制算法。

深入了解Java的GC原理,掌握JVM 性能調優!

停止世界 (STW)

所有垃圾收集都是“Stop the World”事件。這意味着所有應用程式線程都将停止,直到操作完成。垃圾收集始終是“Stop the World”事件。

老年代用于存儲長期存活的對象。通常,為年輕代對象設定一個門檻值,當達到該年齡時,該對象将被移動到老年代。最終需要收集老年代。此事件稱為主垃圾收集。

主垃圾收集也是 Stop the World 事件。通常,主垃圾收集要慢得多,因為它涉及所有活動對象。是以,對于響應式應用程式,應最大程度地減少主要垃圾收集。另請注意,主要垃圾收集的 Stop the World 事件的長度受到用于老年代空間的垃圾收集器類型的影響。

GC 可視化過程

當應用程式啟動并在 Eden 空間上配置設定記憶體時。藍色是活動對象,灰色是死對象(無法到達)。當給定空間已滿時,應用程式嘗試建立另一個對象,并且 JVM 嘗試在 Eden 上配置設定某些内容,但配置設定失敗。這實際上會導緻輕微GC。

深入了解Java的GC原理,掌握JVM 性能調優!

第一次minor GC後,所有存活對象将被移動到Survivor 1,年齡為1,死亡對象将被删除。

深入了解Java的GC原理,掌握JVM 性能調優!

應用程式正在運作,新對象再次在 Eden 空間中配置設定。有些對象在 Eden 空間和 Survivor 1 上都變得無法通路

深入了解Java的GC原理,掌握JVM 性能調優!

在第二次 Minor GC 之後,所有存活對象将被移動到 Survivor 2(來自年齡為 1 的 Eden 和年齡為 2 的 Survivor 1),并且死亡對象将被删除。

深入了解Java的GC原理,掌握JVM 性能調優!

應用程式仍在運作,新對象在 Eden 空間上配置設定,過了一會兒,一些對象從 Eden 和 Survivor 2 都無法通路

深入了解Java的GC原理,掌握JVM 性能調優!

在第三次minor GC之後,随着年齡的增加,所有存活對象将從Eden和Survivor 2移動到Survivor 1,并且死亡對象将被删除。

深入了解Java的GC原理,掌握JVM 性能調優!

在Survivor中存活時間較長的對象,如果年齡大于-XX:MaxTenuringThreshold,将會被提升到老年代(Tuner)

深入了解Java的GC原理,掌握JVM 性能調優!

我們可以使用 VisualVM 的插件 VisualGC 附加到已檢測的 HotSpot JVM,收集并以圖形方式顯示垃圾收集、類加載器和 HotSpot 編譯器性能資料。

深入了解Java的GC原理,掌握JVM 性能調優!

性能基礎知識

通常,在調整 Java 應用程式時,重點是兩個主要目标之一:響應速度和吞吐量。

響應速度

響應能力是指應用程式或系統響應所請求的資料的速度。示例包括:

  • 桌面 UI 響應事件的速度有多快
  • 網站傳回頁面的速度有多快
  • 傳回資料庫查詢的速度有多快

對于注重響應能力的應用程式來說,較長的暫停時間是不可接受的。重點是在短時間内做出響應。

吞吐量

吞吐量側重于在特定時間段内最大化應用程式的工作量。如何測量吞吐量的示例包括:

  • 在給定時間内完成的交易數量。
  • 批處理程式在一小時内可以完成的作業數。
  • 一小時内可以完成的資料庫查詢數量。

對于注重吞吐量的應用程式來說,較長的暫停時間是可以接受的。由于高吞吐量應用程式關注較長時間段的基準,是以不考慮快速響應時間。

GC 有哪些類型?

并發标記清除 (CMS) 垃圾收集

CMS垃圾收集本質上是更新的标記和清除算法。它使用多個線程掃描堆記憶體。它經過修改以利用更快的系統并增強了性能。

它嘗試通過與應用程式線程同時執行大部分垃圾收集工作來最大程度地減少由于垃圾收集而導緻的暫停。它在年輕代中使用并行的 stop-the-world 标記複制算法,在老年代中使用大多數并發的标記清除算法。

要使用 CMS GC,請使用以下 JVM 參數:

-XX:+UseConcMarkSweepGC           

串行垃圾收集

該算法對年輕代使用标記-複制,對老生代使用标記-清除-壓縮。它在單線程上工作。執行時,它會當機所有其他線程,直到垃圾收集操作結束。

由于串行垃圾收集的線程當機性質,它僅适用于非常小的程式垃圾收集。

要使用串行 GC,請使用以下 JVM 參數:

-XX:+UseSerialGC           

并行垃圾收集

與串行GC類似,它在年輕代中使用标記複制,在老年代中使用标記清除緊湊。多個并發線程用于标記和複制/壓縮階段。可以使用 -XX:ParallelGCThreads=N 選項配置線程數。

如果主要的目标是通過有效利用現有系統資源來提高吞吐量,則并行垃圾收集器适用于多核計算機。使用這種方法,可以大大縮短 GC 循環時間。

要使用并行 GC,請使用以下 JVM 參數:

-XX:+UseParallelGC           

G1垃圾收集

G1(垃圾優先)垃圾收集器在 Java 7 中可用,旨在作為 CMS 收集器的長期替代品。G1 收集器是一個并行、并發、增量壓縮的低暫停垃圾收集器。

這種方法涉及将記憶體堆分割成多個小區域(通常為 2048 個)。每個區域都被标記為年輕代(進一步分為eden regions或survivor regions)或老年代。這使得 GC 可以避免一次收集整個堆,而是逐漸解決問題。這意味着一次僅考慮區域的子集。

深入了解Java的GC原理,掌握JVM 性能調優!

G1 持續跟蹤每個區域包含的實時資料量。該資訊用于确定包含最多垃圾的區域;是以首先收集它們。這就是為什麼它被稱為垃圾優先收集。

不幸的是,就像其他算法一樣,壓縮操作是使用 Stop the World 方法進行的。但根據其設計目标,可以為其設定特定的性能目标。還可以配置暫停持續時間,例如在任何給定的秒内不超過 10 毫秒。垃圾優先 GC 将盡最大努力以高機率實作這一目标(但不确定,由于作業系統級别的線程管理)。

如果你想在 Java 7 或 Java 8 機器上使用,請使用 JVM 參數,如下所示:

-XX:+UseSerialGC           

G1 優化選項

-XX:G1HeapRegionSize=16m 堆區域的大小。該值是 2 的幂,範圍從 1MB 到 32MB。目标是根據最小 Java 堆大小擁有大約 2048 個區域。

-XX:MaxGCPauseMillis=200 設定所需最大暫停時間的目标值。預設值為 200 毫秒。指定的值不适合堆大小。

-XX:G1ReservePercent=5 這确定堆中的最小保留量。

-XX:G1ConfidencePercent=75 這是确信度百分比。

-XX:GCPauseIntervalMillis=200 這是每個 MMU 的暫停間隔時間片(以毫秒為機關)。

建議

G1配置

-XX:+UseG1GC \
-XX:+UseStringDeduplication \
-XX:+ParallelRefProcEnabled \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:ParallelGCThreads=8 \
-XX:GCTimeRatio=9 \
-XX:MaxGCPauseMillis=25 \
-XX:MaxGCMinorPauseMillis=5 \
-XX:ConcGCThreads=8 \
-XX:InitiatingHeapOccupancyPercent=70 \
-XX:MaxTenuringThreshold=10 \
-XX:SurvivorRatio=6 \
-XX:-UseAdaptiveSizePolicy \
-XX:MaxMetaspaceSize=256M \
-Xmx4G \
-Xms2G \           

優化結果

深入了解Java的GC原理,掌握JVM 性能調優!

總結

請注意,JVM性能調優是一個複雜的過程,需要結合具體的應用程式特性和需求來進行調優。不同的應用場景可能需要不同的調優政策。在進行JVM性能調優時,應該先進行性能測試和分析,找出性能瓶頸,然後有針對性地進行優化。同時,及時記錄和備份調優前的配置和參數,以便在調優過程中出現問題時能夠恢複到原始狀态。

繼續閱讀