垃圾收集器的秘密:深入理解JVM性能調(diào)優(yōu)
原創(chuàng)作者 | 波哥
審校 | 重樓
Java虛擬機(jī)(JVM)的自動(dòng)內(nèi)存管理是Java開發(fā)者的福音,它通過垃圾收集(GC)機(jī)制自動(dòng)回收不再使用的對(duì)象,極大地簡化了內(nèi)存管理。然而,不恰當(dāng)?shù)腉C配置或不理想的垃圾收集器選擇可能會(huì)對(duì)應(yīng)用性能產(chǎn)生負(fù)面影響。為了優(yōu)化Java應(yīng)用的性能,深入理解GC的原理和策略是至關(guān)重要的。本文筆者將詳細(xì)探討JVM的垃圾收集機(jī)制,包括內(nèi)存模型、GC算法、各種垃圾收集器的特點(diǎn)及其調(diào)優(yōu)策略。
一、JVM內(nèi)存模型深入解析
JVM的內(nèi)存模型是理解GC機(jī)制的基礎(chǔ)。JVM將內(nèi)存分為多個(gè)區(qū)域,主要包括堆(Heap)、方法區(qū)(Method Area)、程序計(jì)數(shù)器(Program Counter Register)、虛擬機(jī)棧(VM Stack)和本地方法棧(Native Method Stack)。
1.堆(Heap)
堆內(nèi)存是Java虛擬機(jī)(JVM)管理的最大一塊內(nèi)存區(qū)域,它被所有線程共享,主要用于存放對(duì)象實(shí)例和數(shù)組。從垃圾收集的角度,堆內(nèi)存進(jìn)一步細(xì)分為新生代(Young Generation)、老年代(Old Generation)以及元空間(Metaspace,在Java 8之后取代了永久代PermGen)。
(1)新生代(Young Generation)
新生代是大多數(shù)新創(chuàng)建的對(duì)象的誕生地。由于對(duì)象的生存周期大多數(shù)較短,新生代的垃圾收集(Minor GC)發(fā)生頻繁但速度快。新生代進(jìn)一步分為三個(gè)區(qū)域:
- Eden區(qū):幾乎所有新生成的對(duì)象首先都是在Eden區(qū)分配。
- 兩個(gè)Survivor區(qū)(S0和S1):用于存放從Eden區(qū)和Survivor區(qū)經(jīng)過一次Minor GC后仍然存活的對(duì)象。在每次Minor GC后,存活的對(duì)象會(huì)被移動(dòng)到一個(gè)Survivor區(qū),而另一個(gè)空閑的Survivor區(qū)將用于下一輪的存活對(duì)象移動(dòng)。
(2)老年代(Old Generation)
隨著時(shí)間的推移,一些在新生代中經(jīng)歷了多次GC依然存活的對(duì)象會(huì)被移動(dòng)到老年代。老年代用于存放應(yīng)用中生命周期長的對(duì)象。相較于新生代,老年代的空間更大,GC發(fā)生的頻率更低,但每次GC的時(shí)間更長。
對(duì)象進(jìn)入老年代(Old Generation)通常是基于它們的存活周期。JVM采用分代垃圾收集策略,其中對(duì)象首先在新生代(Young Generation)分配。隨著垃圾收集的進(jìn)行,只有存活下來的對(duì)象才會(huì)逐步晉升到老年代。具體而言,有幾種情況下對(duì)象會(huì)進(jìn)入到老年代:
(3)經(jīng)歷多次Minor GC后仍然存活的對(duì)象
新生代中的對(duì)象在經(jīng)歷了一定數(shù)量的Minor GC(垃圾收集只針對(duì)新生代的收集稱為Minor GC)后,如果仍然存活,它們會(huì)被移動(dòng)到老年代。JVM中有一個(gè)年齡計(jì)數(shù)器,每當(dāng)對(duì)象在Minor GC后仍然存活,它的年齡就會(huì)增加。當(dāng)對(duì)象的年齡增加到一定閾值(默認(rèn)為15,但可以通過JVM參數(shù)-XX:MaxTenuringThreshold進(jìn)行調(diào)整)時(shí),這個(gè)對(duì)象就會(huì)被晉升到老年代。
(4)大對(duì)象直接分配到老年代
所謂的大對(duì)象是指需要大量連續(xù)內(nèi)存空間的Java對(duì)象,例如那些很大的數(shù)組和長字符串。如果新生代中的Eden區(qū)無法容納一個(gè)新創(chuàng)建的對(duì)象,JVM就會(huì)直接將這個(gè)對(duì)象分配到老年代。這樣做是為了避免在新生代中為大對(duì)象分配內(nèi)存后,進(jìn)行Minor GC時(shí)發(fā)生大量的內(nèi)存復(fù)制操作(因?yàn)樾律褂玫氖菑?fù)制算法)。通過JVM參數(shù)-XX:PretenureSizeThreshold可以設(shè)置大對(duì)象的大小閾值。
(5)動(dòng)態(tài)年齡判斷
在新生代的兩個(gè)Survivor區(qū)之間,對(duì)象每經(jīng)過一次Minor GC就會(huì)年齡增加。如果在Survivor空間中相同年齡所有對(duì)象的大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無需等到-XX:MaxTenuringThreshold設(shè)置的年齡。
(6)空間分配擔(dān)保
在進(jìn)行Minor GC前,虛擬機(jī)會(huì)檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象的總空間。如果這個(gè)條件不能滿足,虛擬機(jī)會(huì)提前將新生代中的部分對(duì)象轉(zhuǎn)移到老年代中,這個(gè)過程稱為“空間分配擔(dān)?!薄D康氖谴_保Minor GC可以順利完成,不會(huì)因?yàn)槔夏甏臻g不足而觸發(fā)更耗時(shí)的Full GC。
(7)元空間(Metaspace)
元空間用于存放類的元數(shù)據(jù)信息,如類的定義信息、常量、靜態(tài)變量等,并使用本地內(nèi)存(而非JVM堆內(nèi)存)。在Java 8之前,這部分?jǐn)?shù)據(jù)被存放在永久代中。元空間的引入是為了避免永久代容易發(fā)生的內(nèi)存溢出問題,并提供更靈活的內(nèi)存管理。
2.方法區(qū)(Method Area)
方法區(qū)(Method Area)是堆的一部分,也被稱為非堆(Non-Heap),它被所有線程共享。方法區(qū)主要用于存放已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
在Java 8及之后的版本中,傳統(tǒng)的永久代(PermGen)被元空間(Metaspace)所取代。與永久代不同,元空間并不在虛擬機(jī)內(nèi)存中,而是使用本地內(nèi)存,因此,元空間的大小只受本地內(nèi)存限制。
方法區(qū)的特點(diǎn)
- 靜態(tài)存儲(chǔ):方法區(qū)存儲(chǔ)的信息相對(duì)靜態(tài),包括類的結(jié)構(gòu)(如運(yùn)行時(shí)常量池、字段和方法數(shù)據(jù))以及方法和構(gòu)造函數(shù)的代碼。
- 全局共享:方法區(qū)被所有線程共享,這意味著它不像堆那樣頻繁地進(jìn)行垃圾收集。實(shí)際上,方法區(qū)的垃圾收集主要針對(duì)常量池的回收和對(duì)類型的卸載。
- 動(dòng)態(tài)擴(kuò)展:雖然方法區(qū)的初始大小有限,但它可以在運(yùn)行時(shí)動(dòng)態(tài)擴(kuò)展,也可以設(shè)置最大空間大小,以防止其過度消耗內(nèi)存。
方法區(qū)的垃圾收集
方法區(qū)的垃圾收集比較少見且難以執(zhí)行,主要涉及兩部分工作:廢棄常量的回收和無用類的卸載。無用類的卸載條件相對(duì)嚴(yán)格,需要同時(shí)滿足以下三個(gè)條件:
- 該類所有的實(shí)例都已經(jīng)被回收,也就是說Java堆中不存在該類的任何實(shí)例。
- 加載該類的ClassLoader已經(jīng)被回收。
- 該類對(duì)應(yīng)的java.lang.Class對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
二、GC算法的探究
GC算法是實(shí)現(xiàn)垃圾收集的具體方法。主要的GC算法包括標(biāo)記-清除(Mark-Sweep)、復(fù)制(Copying)和標(biāo)記-整理(Mark-Compact),下面筆者將詳細(xì)介紹這三種算法的工作原理以及它們的優(yōu)缺點(diǎn)。
1.標(biāo)記-清除算法
(1)工作原理
- 標(biāo)記階段:從一組根對(duì)象(通常是活躍線程的局部變量和輸入?yún)?shù)、靜態(tài)字段等)開始遍歷,標(biāo)記所有從這些根對(duì)象可達(dá)的對(duì)象。
- 清除階段:掃描整個(gè)堆空間,回收所有未被標(biāo)記的對(duì)象所占用的內(nèi)存。
(2)優(yōu)點(diǎn)
- 簡單直接,實(shí)現(xiàn)相對(duì)容易。
- 不需要額外移動(dòng)對(duì)象,減少了額外的開銷。
(3)缺點(diǎn)
- 執(zhí)行過程中會(huì)產(chǎn)生內(nèi)存碎片,導(dǎo)致后續(xù)可能無法為大對(duì)象分配連續(xù)內(nèi)存空間。
- 需要暫停應(yīng)用程序執(zhí)行(Stop-The-World),可能會(huì)導(dǎo)致應(yīng)用響應(yīng)時(shí)間變長。
2.復(fù)制算法
(1)工作原理
- 將可用內(nèi)存劃分為大小相等的兩塊,每次只使用其中一塊。
- 當(dāng)這一塊的內(nèi)存快用完時(shí),將存活的對(duì)象復(fù)制到另一塊空閑區(qū)域。
- 清空已使用的內(nèi)存塊,交換兩個(gè)內(nèi)存區(qū)域的角色。
(2)優(yōu)點(diǎn)
- 解決了標(biāo)記-清除算法中的內(nèi)存碎片問題。
適合存活對(duì)象較少的場(chǎng)景,如新生代的垃圾收集。
(3)缺點(diǎn)
- 需要將存活的對(duì)象復(fù)制到另一塊內(nèi)存區(qū)域,增加了復(fù)制成本,特別是當(dāng)存活對(duì)象較多時(shí)。
內(nèi)存使用效率低,因?yàn)槿魏螘r(shí)候只有一半的內(nèi)存區(qū)域被使用。
3.標(biāo)記-整理算法
(1)工作原理
- 標(biāo)記階段:與標(biāo)記-清除算法相同,從根集合出發(fā)標(biāo)記所有可達(dá)的對(duì)象。
- 整理階段:將所有存活的對(duì)象壓縮到堆的一端,然后清理掉邊界以外的內(nèi)存。
(2)優(yōu)點(diǎn)
- 解決了內(nèi)存碎片問題,為大對(duì)象的分配提供了連續(xù)的內(nèi)存空間。
- 避免了復(fù)制算法中的高成本復(fù)制操作,更適合老年代的垃圾收集。
(3)缺點(diǎn)
- 需要移動(dòng)對(duì)象,并更新對(duì)象引用的位置,增加了額外的開銷。
- 同樣需要暫停應(yīng)用程序執(zhí)行,可能會(huì)影響應(yīng)用的響應(yīng)時(shí)間。
現(xiàn)代JVM實(shí)現(xiàn)通常采用以上基本GC算法的變體或組合,以達(dá)到更高的垃圾收集效率和更低的停頓時(shí)間。例如:G1收集器就是將堆劃分為多個(gè)區(qū)域(Region),并根據(jù)每個(gè)區(qū)域的垃圾回收價(jià)值進(jìn)行增量收集,旨在平衡吞吐量和停頓時(shí)間。ZGC和Shenandoah收集器則采用了基于Region的復(fù)制算法,實(shí)現(xiàn)了幾乎全程并發(fā)的垃圾收集,極大地減少了停頓時(shí)間。
JVM提供了多種垃圾收集器,下面我們大概介紹下目前主流的幾種垃圾回收器及每種收集器的適用場(chǎng)景。
- Serial收集器Serial收集器是最簡單的GC實(shí)現(xiàn),它使用單線程進(jìn)行垃圾收集。在進(jìn)行GC時(shí),需要暫停其他所有工作線程("Stop The World"),因此不適合多處理器環(huán)境或要求低延遲的應(yīng)用。
- Parallel(并行)收集器Parallel收集器類似于Serial收集器,但它使用多線程進(jìn)行垃圾收集,可以顯著減少GC的停頓時(shí)間。它主要關(guān)注達(dá)到一個(gè)可接受的吞吐量(應(yīng)用時(shí)間與GC時(shí)間的比率)。
- Concurrent Mark Sweep(CMS)收集器CMS收集器的目標(biāo)是盡可能減少應(yīng)用停頓時(shí)間。它通過并發(fā)標(biāo)記和并發(fā)清除實(shí)現(xiàn)了這一點(diǎn),但是CMS收集器可能會(huì)產(chǎn)生較多的內(nèi)存碎片。
- G1收集器G1收集器是一種服務(wù)器端的垃圾收集器,旨在替代CMS收集器,它通過將堆劃分為多個(gè)區(qū)域(Region)并并行處理這些區(qū)域來減少停頓時(shí)間,同時(shí)提供了更細(xì)粒度的GC控制。
- ZGC和Shenandoah收集器ZGC和Shenandoah是實(shí)驗(yàn)性的低延遲垃圾收集器,旨在實(shí)現(xiàn)幾乎不停頓的垃圾收集。它們通過使用讀寫屏障和并發(fā)線程來實(shí)現(xiàn)這一目標(biāo),適用于需要極低停頓時(shí)間的應(yīng)用。
三、垃圾收集器的調(diào)優(yōu)實(shí)踐
以上我們?cè)敿?xì)介紹了垃圾回收算法和主流的垃圾回收器,接下來我們?cè)敿?xì)介紹下在實(shí)際應(yīng)用中,該如何根據(jù)具體應(yīng)用特性進(jìn)行調(diào)優(yōu)。以下是一些調(diào)優(yōu)的通用策略:
- 選擇合適的垃圾收集器根據(jù)應(yīng)用的需求(如響應(yīng)時(shí)間要求、吞吐量要求等)和資源限制(如CPU、內(nèi)存大小等),選擇最適合的垃圾收集器。
- 堆大小調(diào)整適當(dāng)?shù)卣{(diào)整堆大小可以平衡GC的頻率和停頓時(shí)間。一般而言,增大堆大小會(huì)減少GC的頻率,但可能增加GC的停頓時(shí)間。
- 監(jiān)控和分析GC日志通過開啟GC日志,可以獲得垃圾收集的詳細(xì)信息,如各階段的耗時(shí)、回收量等。分析這些數(shù)據(jù)可以幫助識(shí)別性能瓶頸和調(diào)優(yōu)方向。
- 細(xì)化GC參數(shù)設(shè)置
JVM提供了豐富的GC相關(guān)參數(shù),通過調(diào)整這些參數(shù)(如新生代與老年代的比例、觸發(fā)Full GC的閾值等),可以微調(diào)垃圾收集的行為,優(yōu)化性能。
深入理解JVM的垃圾收集機(jī)制和各種垃圾收集器的特點(diǎn)是進(jìn)行有效性能調(diào)優(yōu)的前提。通過選擇合適的垃圾收集器并適當(dāng)調(diào)優(yōu),可以顯著提升Java應(yīng)用的性能,滿足不同場(chǎng)景下對(duì)響應(yīng)時(shí)間和吞吐量的需求。記住,沒有一勞永逸的解決方案,性能優(yōu)化是一個(gè)持續(xù)的過程,需要不斷地監(jiān)控、評(píng)估和調(diào)整。
作者介紹
波哥,互聯(lián)行業(yè)從業(yè)10余年,先后擔(dān)任項(xiàng)目總監(jiān)及架構(gòu)師。目前專攻技術(shù),喜歡研究技術(shù)原理。技術(shù)全面,主攻Java,精通JVM底層機(jī)制及Spring全家桶底層框架原理,熟練掌握當(dāng)前主流的中間件、服務(wù)網(wǎng)格等技術(shù)原理。