深入了解Java的GC原理,掌握JVM 性能調(diào)優(yōu)!
對于 Java 開發(fā)人員來說,進行程序的性能優(yōu)化是很有挑戰(zhàn)的工作,也是很有意義的一件事。本篇主要根據(jù) JVM 內(nèi)存模型和垃圾回收的詳細講解,可以更好的理解JVM的調(diào)優(yōu)的根本原理。
JVM內(nèi)存模型
JVM 架構(gòu)
- 類加載器(Classloader):類加載器是JVM的一個子系統(tǒng),用于加載類文件。每當(dāng)我們運行java程序時,它首先由類加載器加載。
- 類(方法)區(qū)(Class(Method) Area):類(方法)區(qū)存儲每個類的結(jié)構(gòu),例如運行時常量池、字段和方法數(shù)據(jù)、方法的代碼。
- 堆(Heap):是分配對象的運行時數(shù)據(jù)區(qū)域。
- 堆棧(Stack):Java 堆棧存儲幀。它保存局部變量和部分結(jié)果,并在方法調(diào)用和返回中發(fā)揮作用。每個線程都有一個私有的 JVM 堆棧,與該線程同時創(chuàng)建。每次調(diào)用方法時都會創(chuàng)建一個新框架。當(dāng)其方法調(diào)用完成時,框架將被銷毀。
- 程序計數(shù)器寄存器(PC):PC(程序計數(shù)器)寄存器包含當(dāng)前正在執(zhí)行的Java虛擬機指令的地址。
- 本機方法堆棧(Native Method Stack):它包含應(yīng)用程序中使用的所有本機方法。
- 執(zhí)行引擎(Execution Engine):它包含:一個虛擬處理器;解釋器:讀取字節(jié)碼流然后執(zhí)行指令。
- Just-In-Time(JIT)編譯器:它用于提高性能。JIT 同時編譯具有相似功能的字節(jié)碼部分,從而減少編譯所需的時間。這里,術(shù)語“編譯器”是指從Java虛擬機(JVM)的指令集到特定CPU的指令集的翻譯器。
- Java 本機接口:Java 本機接口 (JNI) 是一個框架,提供與用其他語言(如 C、C++、匯編等)編寫的另一個應(yīng)用程序進行通信的接口。Java 使用 JNI 框架將輸出發(fā)送到控制臺或與操作系統(tǒng)交互。
應(yīng)該已經(jīng)使用了一些像這樣的 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”;該參數(shù)在 64 位 JVM 中默認使用。
- -Xms - 堆的初始空間。
- -Xmx - 堆的最大空間。
- -XX:NewSize - 初始新空間。將新大小設(shè)置為總堆的一半通常比使用較小的新大小提供更好的性能。
- -XX:MaxNewSize - 最大新空間。
- -XX:MetaspaceSize - 靜態(tài)內(nèi)容的初始空間。
- -XX:MaxMetaspaceSize - 靜態(tài)內(nèi)容的最大空間。
- -XX:InitialCodeCacheSize - JIT 編譯代碼的初始空間。代碼緩存太?。J為 48m)會降低性能,因為 JIT 無法優(yōu)化高頻方法。
- -XX:ReservedCodeCacheSize - JIT 編譯代碼的最大空間。
- -XX:MaxTenuringThreshold - 在升級到老年代空間之前,將幸存者保留在幸存者空間中最多 15 次垃圾回收。
那么 JVM 是如何駐留在內(nèi)存上的?JVM 消耗主機操作系統(tǒng)內(nèi)存上的可用空間。
然而,在 JVM 內(nèi)部,存在獨立的內(nèi)存空間(堆、非堆、緩存),以存儲運行時數(shù)據(jù)和編譯后的代碼。
堆內(nèi)存
- 堆分為兩部分:Young Generation 和 Old Generation
- JVM 啟動時分配堆(初始大?。?Xms)
- 應(yīng)用程序運行時堆大小增加/減少
- 堆的最大空間:-Xmx
以下是有關(guān)服務(wù)器應(yīng)用程序堆大小的一般準則:
- 除非遇到暫停問題,否則請嘗試為虛擬機授予盡可能多的內(nèi)存。默認大小通常太小。
- 將-Xms和-Xmx設(shè)置為相同的值可以消除虛擬機中最重要的大小調(diào)整決策,從而提高可預(yù)測性。但是如果設(shè)置相同大小出了錯誤,虛擬機將無法進行補償。
- 一般來說,隨著處理器數(shù)量的增加,內(nèi)存也會隨之增加,因為分配可以并行進行。
年輕代(Young Generation)
- 這是為包含新分配的對象而保留的
- Young Gen 包括三個部分——Eden Memory 和兩個 Survivor Memory 空間(S0、S1)
- 大多數(shù)新創(chuàng)建的對象都會進入Eden space。
- 當(dāng) Eden 空間充滿對象時,將執(zhí)行 Minor GC(又名 Young Collection),并將所有幸存者對象移動到幸存者空間之一。
- Minor GC 還會檢查幸存者對象并將它們移動到其他幸存者空間。所以在某一時刻,幸存者的一個空間總是空著的。
- 經(jīng)過多次GC后幸存的對象會被移至Old代內(nèi)存空間。通常,這是通過在年輕代對象有資格晉升到老年代之前設(shè)置年齡閾值(-XX:MaxTenuringThreshold)來完成的。
老年代(Old Generation)
- 這是為包含在多輪 Minor GC 后仍能存活的長壽命對象而保留的
- 當(dāng) Old Gen 空間滿時,將執(zhí)行 Major GC(又名 Old Collection)(通常需要更長的時間)
非堆內(nèi)存
- 這包括永久生成(自 Java 8 起被 Metaspace 取代)
- Perm Gen 存儲每個類的結(jié)構(gòu),例如運行時常量池、字段和方法數(shù)據(jù)、方法和構(gòu)造函數(shù)的代碼以及內(nèi)部字符串
- 可以使用 -XX:PermSize 和 -XX:MaxPermSize 更改其大小
高速緩存存儲器
- 這包括代碼緩存
- 存儲JIT編譯器生成的編譯代碼(即本機代碼)、JVM內(nèi)部結(jié)構(gòu)、加載的分析器代理代碼和數(shù)據(jù)等。
- 當(dāng)代碼緩存超過閾值時,它會被刷新(GC 不會重新定位對象)。
什么是GC?
Java 通過一個稱為垃圾收集器的程序提供自動內(nèi)存管理。
“移除不再使用的對象?!?/span>
上面的一切都是在堆中完成的,堆是運行時動態(tài)內(nèi)存分配的空間,用于包含所有 java 對象。除了堆之外,還有堆棧,其中包含支持線程執(zhí)行的局部變量和函數(shù)調(diào)用。
Java 垃圾收集的實際工作原理
許多人認為垃圾收集會收集并丟棄死對象。事實上,Java 垃圾收集的作用恰恰相反!活動對象被跟蹤,其他所有對象都被指定為垃圾。這種根本性的誤解可能會導(dǎo)致許多性能問題。
讓我們從堆開始,它是用于動態(tài)分配的內(nèi)存區(qū)域。在大多數(shù)配置中,操作系統(tǒng)會提前分配堆,以便在程序運行時由 JVM 管理。這有幾個重要的影響:
- 對象創(chuàng)建速度更快,因為不需要每個對象都與操作系統(tǒng)進行全局同步。分配只是聲明內(nèi)存數(shù)組的某些部分并將偏移指針向前移動(參見圖 2.1)。下一個分配從此偏移量開始,并聲明數(shù)組的下一部分。
- 當(dāng)不再使用某個對象時,垃圾收集器會回收底層內(nèi)存并將其重新用于將來的對象分配。這意味著沒有顯式刪除,也沒有內(nèi)存返回給操作系統(tǒng)。
新對象簡單地分配在已用堆的末尾
一旦某個對象不再被引用并且因此應(yīng)用程序代碼無法訪問該對象,垃圾收集器就會將其刪除并回收未使用的內(nèi)存。
垃圾收集根——所有對象樹的來源
每個對象樹必須有一個或多個根對象。只要應(yīng)用程序可以到達這些根,那么整棵樹都是可以到達的。但是這些根對象什么時候被認為是可達的呢?稱為垃圾收集根,它是特殊對象始終是可訪問的,任何在其根處具有垃圾收集根的對象也是如此。
Java中有四種GC root:
- 局部變量通過線程的堆棧保持活動狀態(tài)。這不是真實的對象虛擬引用,因此不可見。無論如何,局部變量都是 GC 根。
- 活動的 Java 線程始終被視為活動對象,因此是 GC 根。這對于線程局部變量尤其重要。
- 靜態(tài)變量由它們的類引用。這一事實使它們成為事實上的 GC 根。類本身可以被垃圾收集,這將刪除所有引用的靜態(tài)變量。
- JNI 引用是本機代碼作為 JNI 調(diào)用的一部分創(chuàng)建的 Java 對象。這樣創(chuàng)建的對象會被特殊對待,因為 JVM 不知道它是否被本機代碼引用。
GC 根是 JVM 本身引用的對象,因此可以防止其他所有對象被垃圾收集。
因此,一個簡單的 Java 應(yīng)用程序具有以下 GC 根:
- main方法中的局部變量
- 主線程
- 主類的靜態(tài)變量
標(biāo)記并清除垃圾
標(biāo)記可達對象
為了確定哪些對象不再使用,JVM 間歇性地運行所謂的“標(biāo)記和清除”算法。正如所直覺的,這是一個簡單的兩步過程:
- 該算法從 GC 根開始遍歷所有對象引用,并將找到的每個對象標(biāo)記為活動對象。
- 所有未被標(biāo)記對象占用的堆內(nèi)存都會被回收。它只是被簡單地標(biāo)記為空閑,基本上清除了未使用的對象。
活動對象在上圖中表示為藍色。當(dāng)標(biāo)記階段結(jié)束時,每個活動對象都被標(biāo)記。因此,所有其他對象(上圖中的灰色數(shù)據(jù)結(jié)構(gòu))都無法從 GC 根訪問,這意味著應(yīng)用程序無法再使用無法訪問的對象。此類對象被視為垃圾,GC 應(yīng)在以下階段中刪除它們。
標(biāo)記階段需要注意以下重要方面:
- 需要停止應(yīng)用程序線程才能進行標(biāo)記,因為如果圖表一直在不斷變化,就無法真正遍歷圖表。當(dāng)應(yīng)用程序線程暫時停止以便 JVM 可以進行內(nèi)務(wù)活動時,這種情況稱為安全點,導(dǎo)致 Stop The World 暫停。安全點可以因不同的原因而被觸發(fā),但垃圾收集是迄今為止引入安全點的最常見原因。
- 此暫停的持續(xù)時間既不取決于堆中對象的總數(shù),也不取決于堆的大小,而是取決于活動對象的數(shù)量。因此增加堆的大小并不會直接影響標(biāo)記階段的持續(xù)時間。
- 當(dāng)標(biāo)記階段完成后,GC就可以進行下一步并開始刪除不可達的對象。
刪除未使用的對象
對于不同的 GC 算法,未使用對象的刪除略有不同,但所有此類 GC 算法都可以分為三步:清除(sweeping)、壓縮(compacting)和復(fù)制(copying)。
Sweep
標(biāo)記和清除算法在概念上使用最簡單的垃圾處理方法,即忽略此類對象。這意味著在標(biāo)記階段完成后,未訪問對象占用的所有空間都被視為空閑,因此可以重用以分配新對象。
該方法需要使用所謂的空閑列表記錄每個空閑區(qū)域及其大小??臻e列表的管理增加了對象分配的開銷。這種方法還有另一個弱點——可能存在大量空閑區(qū)域,但如果沒有一個區(qū)域足夠大來容納分配,分配仍然會失?。ㄔ?Java 中會出現(xiàn) OutOfMemoryError 錯誤)。
它通常被稱為標(biāo)記-清除算法。
Compact
Mark-Sweep-Compact算法通過將所有標(biāo)記的對象 (即活動的對象)移動到內(nèi)存區(qū)域的開頭來解決Mark-and-Sweep算法的缺點。這種方法的缺點是增加了GC暫停時間,因為我們需要將所有對象復(fù)制到一個新位置,并更新對這些對象的所有引用。Markand Sweep的好處也是顯而易見的--在這樣一個壓縮操作之后,通過指針碰撞,新對象的分配再次變得非常便宜。使用這種方法,空閑空間的位置總是已知的,也不會觸發(fā)碎片問題。
它通常被稱為標(biāo)記-壓縮算法。
Copy
標(biāo)記和復(fù)制算法非常類似于標(biāo)記和壓縮,因為它們也重新定位所有活動對象。重要的區(qū)別在于,對象搬遷的目標(biāo)是不同的記憶區(qū)域,作為幸存對象的新家。標(biāo)記和復(fù)制方法具有一些優(yōu)點,因為復(fù)制可以與標(biāo)記在同一階段同時發(fā)生。缺點是需要多一個內(nèi)存區(qū)域,該內(nèi)存區(qū)域應(yīng)該足夠大以容納幸存的對象。
它通常被稱為標(biāo)記復(fù)制算法。
停止世界 (STW)
所有垃圾收集都是“Stop the World”事件。這意味著所有應(yīng)用程序線程都將停止,直到操作完成。垃圾收集始終是“Stop the World”事件。
老年代用于存儲長期存活的對象。通常,為年輕代對象設(shè)置一個閾值,當(dāng)達到該年齡時,該對象將被移動到老年代。最終需要收集老年代。此事件稱為主垃圾收集。
主垃圾收集也是 Stop the World 事件。通常,主垃圾收集要慢得多,因為它涉及所有活動對象。因此,對于響應(yīng)式應(yīng)用程序,應(yīng)最大程度地減少主要垃圾收集。另請注意,主要垃圾收集的 Stop the World 事件的長度受到用于老年代空間的垃圾收集器類型的影響。
GC 可視化過程
當(dāng)應(yīng)用程序啟動并在 Eden 空間上分配內(nèi)存時。藍色是活動對象,灰色是死對象(無法到達)。當(dāng)給定空間已滿時,應(yīng)用程序嘗試創(chuàng)建另一個對象,并且 JVM 嘗試在 Eden 上分配某些內(nèi)容,但分配失敗。這實際上會導(dǎo)致輕微GC。
第一次minor GC后,所有存活對象將被移動到Survivor 1,年齡為1,死亡對象將被刪除。
應(yīng)用程序正在運行,新對象再次在 Eden 空間中分配。有些對象在 Eden 空間和 Survivor 1 上都變得無法訪問
在第二次 Minor GC 之后,所有存活對象將被移動到 Survivor 2(來自年齡為 1 的 Eden 和年齡為 2 的 Survivor 1),并且死亡對象將被刪除。
應(yīng)用程序仍在運行,新對象在 Eden 空間上分配,過了一會兒,一些對象從 Eden 和 Survivor 2 都無法訪問
在第三次minor GC之后,隨著年齡的增加,所有存活對象將從Eden和Survivor 2移動到Survivor 1,并且死亡對象將被刪除。
在Survivor中存活時間較長的對象,如果年齡大于-XX:MaxTenuringThreshold,將會被提升到老年代(Tuner)
我們可以使用 VisualVM 的插件 VisualGC 附加到已檢測的 HotSpot JVM,收集并以圖形方式顯示垃圾收集、類加載器和 HotSpot 編譯器性能數(shù)據(jù)。
性能基礎(chǔ)知識
通常,在調(diào)整 Java 應(yīng)用程序時,重點是兩個主要目標(biāo)之一:響應(yīng)速度和吞吐量。
響應(yīng)速度
響應(yīng)能力是指應(yīng)用程序或系統(tǒng)響應(yīng)所請求的數(shù)據(jù)的速度。示例包括:
- 桌面 UI 響應(yīng)事件的速度有多快
- 網(wǎng)站返回頁面的速度有多快
- 返回數(shù)據(jù)庫查詢的速度有多快
對于注重響應(yīng)能力的應(yīng)用程序來說,較長的暫停時間是不可接受的。重點是在短時間內(nèi)做出響應(yīng)。
吞吐量
吞吐量側(cè)重于在特定時間段內(nèi)最大化應(yīng)用程序的工作量。如何測量吞吐量的示例包括:
- 在給定時間內(nèi)完成的交易數(shù)量。
- 批處理程序在一小時內(nèi)可以完成的作業(yè)數(shù)。
- 一小時內(nèi)可以完成的數(shù)據(jù)庫查詢數(shù)量。
對于注重吞吐量的應(yīng)用程序來說,較長的暫停時間是可以接受的。由于高吞吐量應(yīng)用程序關(guān)注較長時間段的基準,因此不考慮快速響應(yīng)時間。
GC 有哪些類型?
并發(fā)標(biāo)記清除 (CMS) 垃圾收集
CMS垃圾收集本質(zhì)上是升級的標(biāo)記和清除算法。它使用多個線程掃描堆內(nèi)存。它經(jīng)過修改以利用更快的系統(tǒng)并增強了性能。
它嘗試通過與應(yīng)用程序線程同時執(zhí)行大部分垃圾收集工作來最大程度地減少由于垃圾收集而導(dǎo)致的暫停。它在年輕代中使用并行的 stop-the-world 標(biāo)記復(fù)制算法,在老年代中使用大多數(shù)并發(fā)的標(biāo)記清除算法。
要使用 CMS GC,請使用以下 JVM 參數(shù):
-XX:+UseConcMarkSweepGC
串行垃圾收集
該算法對年輕代使用標(biāo)記-復(fù)制,對老生代使用標(biāo)記-清除-壓縮。它在單線程上工作。執(zhí)行時,它會凍結(jié)所有其他線程,直到垃圾收集操作結(jié)束。
由于串行垃圾收集的線程凍結(jié)性質(zhì),它僅適用于非常小的程序垃圾收集。
要使用串行 GC,請使用以下 JVM 參數(shù):
-XX:+UseSerialGC
并行垃圾收集
與串行GC類似,它在年輕代中使用標(biāo)記復(fù)制,在老年代中使用標(biāo)記清除緊湊。多個并發(fā)線程用于標(biāo)記和復(fù)制/壓縮階段??梢允褂?-XX:ParallelGCThreads=N 選項配置線程數(shù)。
如果主要的目標(biāo)是通過有效利用現(xiàn)有系統(tǒng)資源來提高吞吐量,則并行垃圾收集器適用于多核計算機。使用這種方法,可以大大縮短 GC 循環(huán)時間。
要使用并行 GC,請使用以下 JVM 參數(shù):
-XX:+UseParallelGC
G1垃圾收集
G1(垃圾優(yōu)先)垃圾收集器在 Java 7 中可用,旨在作為 CMS 收集器的長期替代品。G1 收集器是一個并行、并發(fā)、增量壓縮的低暫停垃圾收集器。
這種方法涉及將內(nèi)存堆分割成多個小區(qū)域(通常為 2048 個)。每個區(qū)域都被標(biāo)記為年輕代(進一步分為eden regions或survivor regions)或老年代。這使得 GC 可以避免一次收集整個堆,而是逐步解決問題。這意味著一次僅考慮區(qū)域的子集。
G1 持續(xù)跟蹤每個區(qū)域包含的實時數(shù)據(jù)量。該信息用于確定包含最多垃圾的區(qū)域;所以首先收集它們。這就是為什么它被稱為垃圾優(yōu)先收集。
不幸的是,就像其他算法一樣,壓縮操作是使用 Stop the World 方法進行的。但根據(jù)其設(shè)計目標(biāo),可以為其設(shè)置特定的性能目標(biāo)。還可以配置暫停持續(xù)時間,例如在任何給定的秒內(nèi)不超過 10 毫秒。垃圾優(yōu)先 GC 將盡最大努力以高概率實現(xiàn)這一目標(biāo)(但不確定,由于操作系統(tǒng)級別的線程管理)。
如果你想在 Java 7 或 Java 8 機器上使用,請使用 JVM 參數(shù),如下所示:
-XX:+UseSerialGC
G1 優(yōu)化選項
- -XX:G1HeapReginotallow=16m 堆區(qū)域的大小。該值是 2 的冪,范圍從 1MB 到 32MB。目標(biāo)是根據(jù)最小 Java 堆大小擁有大約 2048 個區(qū)域。
- -XX:MaxGCPauseMillis=200 設(shè)置所需最大暫停時間的目標(biāo)值。默認值為 200 毫秒。指定的值不適合堆大小。
- -XX:G1ReservePercent=5 這確定堆中的最小保留量。
- -XX:G1Cnotallow=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 \
優(yōu)化結(jié)果
總結(jié)
請注意,JVM性能調(diào)優(yōu)是一個復(fù)雜的過程,需要結(jié)合具體的應(yīng)用程序特性和需求來進行調(diào)優(yōu)。不同的應(yīng)用場景可能需要不同的調(diào)優(yōu)策略。在進行JVM性能調(diào)優(yōu)時,應(yīng)該先進行性能測試和分析,找出性能瓶頸,然后有針對性地進行優(yōu)化。同時,及時記錄和備份調(diào)優(yōu)前的配置和參數(shù),以便在調(diào)優(yōu)過程中出現(xiàn)問題時能夠恢復(fù)到原始狀態(tài)。