JVM

JVM架構(gòu)

當(dāng)一個程序啟動之前,它的class會被類裝載器裝入方法區(qū)(Permanent區(qū)),執(zhí)行引擎讀取方法區(qū)的字節(jié)碼自適應(yīng)解析,邊解析邊運行,然后pc寄存器指向了main函數(shù)所在位置,虛擬機開始為main函數(shù)在Java棧中預(yù)留一個棧幀(每個方法都對應(yīng)一個棧幀),然后開始跑main函數(shù),main函數(shù)里的代碼被執(zhí)行引擎映射成本地操作系統(tǒng)里相應(yīng)的實現(xiàn),然后調(diào)用本地方法接口,本地方法運行的時候,操縱系統(tǒng)會為本地方法分配本地方法棧,用來儲存一些臨時變量,然后運行本地方法,調(diào)用操作系統(tǒng)API。

執(zhí)行引擎中的GC(垃圾收集器)主要作用域運行時數(shù)據(jù)區(qū)的方法區(qū)和堆。

一些概念

通用GC概念

垃圾:Garbage(名詞),在系統(tǒng)運行過程當(dāng)中所產(chǎn)生的一些無用的對象,這些對象占據(jù)著一定的內(nèi)存空間,如果長期不被釋放,可能導(dǎo)致OOM。

垃圾收集器:Garbage Collector(名詞),負責(zé)回收垃圾對象的垃圾收集器

垃圾回收:Garbage Collect(動詞),垃圾收集器工作時,對垃圾進行回收

垃圾回收算法/GC算法:不同的GC算法,它們的垃圾回收工作模式不同(比如串行、并行等)

引用計數(shù)算法(Reference Counting)

標(biāo)記-清除算法(Mark-Sweep)

復(fù)制算法(Copy)

標(biāo)記-整理算法(Mark-Compact)

標(biāo)記-清除-整理算法(Mark-Sweep-Compact)

GC算法優(yōu)點缺點存活對象移動內(nèi)存碎片適用場景

引用計數(shù)實現(xiàn)簡單不能處理循環(huán)引用

標(biāo)記清除不需要額外空間兩次掃描,耗時嚴(yán)重NY舊生代

復(fù)制沒有標(biāo)記和清除需要額外空間YN新生代

標(biāo)記整理沒有內(nèi)存碎片需要移動對象的成本YN舊生代

這幾種算法中存活對象沒有移動的算法只有:標(biāo)記-清除算法。復(fù)制算法會將存活對象移動到另一塊內(nèi)存區(qū),標(biāo)記整理算法會將存活對象移動到邊界位置

垃圾回收線程/GC線程:垃圾收集器工作時的線程。

應(yīng)用程序和GC都是一種線程,以Java的main方法為例:應(yīng)用程序的線程指的是main方法的主線程,GC線程是JVM的內(nèi)部線程。

在GC過程中,如果GC線程必須暫停應(yīng)用程序線程(用戶線程),則發(fā)生Stop the World。當(dāng)然也可以允許GC線程和應(yīng)用程序線程一起運行,即GC并不會暫停應(yīng)用程序的線程。

串行、并行、并發(fā):串行和并行指的是垃圾收集器工作時暫停應(yīng)用程序(發(fā)生Stop the World),使用單核CPU(串行)還是多核CPU(并行)。

串行(Serial):使用單核CPU串行地進行垃圾收集

并行(Parallel):使用多CPU并行地進行垃圾收集,并行是GC線程有多個,但在運行GC線程時,用戶線程是阻塞的

并發(fā)(Concurrent):垃圾收集時不會暫停應(yīng)用程序線程,大部分階段用戶線程和GC線程都在運行,我們稱垃圾收集器和應(yīng)用程序是并發(fā)運行的。

概念Stop the World單線程/多線程

串行YGC線程是單線程

并行YGC線程是多線程

并發(fā)NGC線程和應(yīng)用程序線程是多線程

在Java中有并發(fā)編程的概念,并發(fā)編程中有多線程的概念。通常并發(fā)指的是不同類型的線程可以同時運行(比如GC線程和用戶線程并發(fā)地運行),而并行指的是相同類型的線程采用多線程模式運行(比如GC線程使用多個CPU并行地運行)。

GC暫停/Stop The World/STW:不管選擇哪種GC算法,Stop-the-world都是不可避免的。Stop-the-world意味著從應(yīng)用中停下來并進入到GC執(zhí)行過程中去。一旦Stop-the-world發(fā)生,除了GC所需的線程外,其他線程都將停止工作,中斷了的線程直到GC任務(wù)結(jié)束才繼續(xù)它們的任務(wù)。GC調(diào)優(yōu)通常就是為了改善stop-the-world的時間(盡量減少STW對應(yīng)用程序造成的暫停時間)。

垃圾對象:對象如果沒有在使用,認(rèn)為是垃圾對象,那么怎么判定有沒有在使用?

一個在使用的對象(被引用的對象):程序的某個部分依然維系者一個指向該對象的指針

一個沒有使用的對象(未被引用的對象):該對象不再被你程序的任何部分引用,所以被這些不再使用的對象占用的內(nèi)存可以(被垃圾收集器)得到回收

具體的實現(xiàn)方式:

引用計數(shù)算法:對象的引用計數(shù)=0,表示沒有對象引用它,可以作為垃圾對象(該方法無法處理循環(huán)引用)

根搜索算法:當(dāng)前對象到根對象沒有一條可達的路徑,可以作為垃圾對象(JVM采用此方法)

引用計數(shù)算法和根搜索算法

引用計數(shù)

引用計數(shù)算法:每個對象都有一個引用計數(shù)器,當(dāng)有對象引用它時,計數(shù)器+1;當(dāng)引用失效時,計數(shù)器-1;任何時刻計數(shù)器為0時就是不可能再被使用的。

下圖中左圖是對象的引用關(guān)系,中圖有一個引用失效,右圖是清理引用計數(shù)器=0的對象后。

但是這種方式的缺點是:

引用和去引用伴隨加法和減法,影響性能

對于循環(huán)引用的對象無法進行回收

下面的3個圖中,最右圖三個對象的循環(huán)引用的計數(shù)器都不為0,但是他們對于根對象都已經(jīng)不可達了,但是無法釋放。

根搜索

解決循環(huán)引用的辦法是:使用根搜索算法來判定對象是否需要被回收,只要對象沒有一條到根對象的可達路徑,就可以被回收。

所以問題轉(zhuǎn)換為:怎么定義根對象?在Java中可以作為GC Roots的對象:

虛擬機棧(左上)的棧幀的局部變量表所引用的對象

本地方法棧(右中)的JNI所引用的對象

方法區(qū)(右下)的靜態(tài)變量和常量所引用的對象

示例

循環(huán)引用的程序?qū)嵗绻捎靡糜嫈?shù),無法被垃圾收集器回收

使用根搜索算法,則可以正常回收

Tracing GC算法

Tracing GC算法主要包括:

標(biāo)記清理/標(biāo)記清除:標(biāo)記垃圾對象,然后清理垃圾對象

復(fù)制算法:標(biāo)記垃圾對象和非垃圾對象,將非垃圾對象移動到某個空閑的內(nèi)存塊

標(biāo)記壓縮/標(biāo)記整理:標(biāo)記垃圾對象和非垃圾對象,將非垃圾對象移動在一起

通常還會存在標(biāo)記清理和標(biāo)記壓縮結(jié)合起來的:標(biāo)記-清理-壓縮算法

結(jié)合根搜索算法定義的對象可達性,對應(yīng)的垃圾收集算法如下(左圖是垃圾收集前,右圖是垃圾收集后):

A.標(biāo)記-清理算法(Mark-Sweep Collector)

步驟:

標(biāo)記階段:根據(jù)可達性分析對不可達對象進行標(biāo)記,即標(biāo)記出所有需要被回收的對象

清理階段:標(biāo)記完成后統(tǒng)一清理這些對象,即回收被標(biāo)記的對象所占用的空間

缺點:

標(biāo)記和清理的效率都不算高(因為垃圾對象比較少,大部分對象都不是垃圾)

會產(chǎn)生大量的內(nèi)存碎片,碎片太多可能會導(dǎo)致后續(xù)過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發(fā)新的一次垃圾收集動作

適用場景:基于Mark-Sweep的GC多用于老年代。

B.復(fù)制算法(Copy Collector)

適用場景:新生代GC

C.標(biāo)記-壓縮算法(Mark-Compact Collector)

步驟:在完成標(biāo)記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動,然后清理掉端邊界以外的內(nèi)存

在標(biāo)記好待回收對象后,將存活的對象移至一端

然后對剩余的部分進行回收

優(yōu)點:

可以解決內(nèi)存碎片的問題

適用場景:基于Mark-Compact的GC多用于老年代

D.標(biāo)記-清理-壓縮算法(Mark-Sweep-Compact Collector)

結(jié)合使用標(biāo)記清理算法(Mark-Sweep)和標(biāo)記壓縮算法(Mark-Compact)

并不是每次標(biāo)記清理都會執(zhí)行壓縮,而是多次執(zhí)行GC后,才會執(zhí)行一次Compact

優(yōu)點:

相對于標(biāo)記清理和標(biāo)記壓縮算法,可以減少移動對象的成本(并不是說不會移動對象,只要有壓縮就一定會移動對象,只不過壓縮不是很頻繁)

JVM GC

前面我們分析了垃圾收集器的幾種算法,在Java中,因為對象創(chuàng)建在堆中,垃圾收集時,垃圾收集器就應(yīng)該掃描堆中的對象,執(zhí)行垃圾收集工作。

基于分代理論的垃圾回收

JVM的垃圾回收器基于以下兩個假設(shè):

大多數(shù)對象很快就會變得不可達,即很多對象的生存時間都很短

只有極少數(shù)情況會出現(xiàn)舊對象(老年代對象)持有新對象(新生代)的引用,即新生對象很少引用生存時間長的對象

問題1:到底是老年代對象引用新生代對象,還是新生代對象引用老年代對象?

問題2:引用和持有引用有什么關(guān)系,比如A引用了B,和A持有B的引用。

這兩條假設(shè)被稱為”弱分代假設(shè)”。為了證明此假設(shè),在HotSpot VM中物理內(nèi)存空間被劃分為兩部分:新生代(Young generation)和老年代(Old generation)。

新生代:大部分新創(chuàng)建的對象分配在新生代。因為大部分對象很快就會變得不可達,所以它們被分配在新生代,然后消失不再。當(dāng)對象從新生代移除時,我們稱之為”Minor GC”。

老年代:在新生代中存活的對象達到一定年齡閾值時會被復(fù)制到老年代。一般來說老年代的內(nèi)存空間比新生代大,所以在老年代GC發(fā)生的頻率較新生代低一些。當(dāng)對象從老年代被移除時,我們稱之為”Major GC”(或者Full GC)。

概念

分代:將JVM的堆內(nèi)存分成多個代(generation)。

新生代/年輕代:Java對象存活周期短命的對象放在新生代

由Eden、兩塊相同大小的Survivor區(qū)構(gòu)成,to總為空

一般在Eden分配對象,優(yōu)化:ThreadLocalAllocationBuffer

保存80%-90%生命周期較短的對象,GC頻率高,采用效率較高的復(fù)制算法

舊生代/老年代/年老代:Java對象存活周期長命的對象放在老年代

存放新生代中經(jīng)歷多次GC仍然存活的對象

新建的對象也有可能直接在舊生代分配,取決于具體GC的實現(xiàn)

GC頻率相對降低,標(biāo)記(mark)、清理(sweep)、壓縮(compaction)算法的各種結(jié)合和優(yōu)化

Minor GC/Majar GC/Full GC

Minor GC 清理的是新生代空間,因此也叫做新生代GC

Major GC 清理的是老年代的空間,因此也叫做老年代GC

Full GC 清理的是整個堆:包括新生代、老年代空間

JVM的分代回收算法:根據(jù)不同代的特點采取最適合的收集算法,老年代的特點是每次垃圾收集時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量的對象需要被回收。

新生代:由于新生代產(chǎn)生很多臨時對象,大量對象需要進行回收,所以采用復(fù)制算法是最高效的:存活對象少,回收對象多

老年代:回收的對象很少,都是經(jīng)過幾次標(biāo)記后都不是可回收的狀態(tài)轉(zhuǎn)移到老年代的,所以僅有少量對象需要回收,故采用標(biāo)記清除或者標(biāo)記整理算法:存活對象多,回收對象少

不同代的GC算法選擇:把Java堆分為新生代和老年代:短命對象歸為新生代,長命對象歸為老年代。

少量對象存活,適合復(fù)制算法:在新生代中,每次GC時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成GC。

大量對象存活,適合用標(biāo)記-清理/標(biāo)記-整理:在老年代中,因為對象存活率高、沒有額外空間對他進行分配擔(dān)保,就必須使用“標(biāo)記-清理”/“標(biāo)記-整理”算法進行GC。

新生代和老年代使用不同的GC算法(不同區(qū)中對象的存活特性不同),基于大多數(shù)新生對象都會在GC中被收回,新生代的GC使用復(fù)制算法

對象一般出生在Eden區(qū),年輕代GC過程中,對象在2個幸存區(qū)之間移動,如果幸存區(qū)中的對象存活到適當(dāng)?shù)哪挲g,會被移動(提升)到老年代。

當(dāng)對象在老年代死亡時,就需要更高級別的GC,更重量級的GC算法,復(fù)制算法不適用于老年代,因為沒有多余的空間用于復(fù)制

Q&A

GC需要完成的事情

哪些內(nèi)存需要回收?

什么時候回收?

如何回收?

JVM內(nèi)存區(qū)域中的程序計數(shù)器、虛擬機棧、本地方法棧這3個區(qū)域隨著線程而生,線程而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執(zhí)行著出棧和入棧的操作,每個棧幀中分配多少內(nèi)存基本是在類結(jié)構(gòu)確定下來時就已知的。在這幾個區(qū)域不需要過多考慮回收的問題,因為方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟著回收了。

而Java堆和方法區(qū)則不同,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不同,一個方法中的多個分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運行期間時才能知道會創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收都是動態(tài)的,GC關(guān)注的也是這部分內(nèi)存。

Java的內(nèi)存管理:Java的內(nèi)存管理實際上就是對象的管理,其中包括對象的分配和釋放。分配對象使用new關(guān)鍵字;釋放對象時,只要將對象所有引用賦值為null,讓程序不能夠再訪問到這個對象,我們稱該對象為”不可達的”,GC將負責(zé)回收所有”不可達”對象的內(nèi)存空間。

對于GC來說,當(dāng)程序員創(chuàng)建對象時,GC就開始監(jiān)控這個對象的地址、大小以及使用情況。通常,GC采用有向圖的方式記錄和管理堆中的所有對象。通過這種方式確定哪些對象是”可達的”,哪些對象是”不可達的”。當(dāng)GC確定一些對象為”不可達”時,GC就有責(zé)任回收這些內(nèi)存空間。

為什么需要GC?:忘記或者錯誤的內(nèi)存回收會導(dǎo)致程序或系統(tǒng)的不穩(wěn)定甚至崩潰,Java語言沒有提供釋放已分配內(nèi)存的顯示操作方法,但是Java提供的GC功能可以自動監(jiān)測對象是否超過作用域從而達到自動回收內(nèi)存的目的。在使用Java時,不需要在程序代碼中顯式地釋放內(nèi)存空間,垃圾回收器會幫你找到不再需要的(垃圾)對象并把他們移出。

為什么GC時需要暫停應(yīng)用程序?:垃圾回收的時候,需要整個堆的引用狀態(tài)保持不變,否則判定是垃圾,等稍后回收的時候它又被引用了,這就全亂套了。所以GC的時候,其他所有的程序執(zhí)行處于暫停狀態(tài),卡住了。幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微,所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。

以引用計數(shù)的方式回收垃圾對象為例,應(yīng)用程序的線程需要被暫停才能完成回收,因為如果引用狀態(tài)一直在變的話,垃圾收集器就無法準(zhǔn)確地計數(shù)(統(tǒng)計對象的引用次數(shù))。垃圾回收時要保證內(nèi)存中所有對象的引用狀態(tài)不變,所以GC時其他所有的程序處于暫停狀態(tài)。

增量式GC和普通GC的區(qū)別:GC在JVM中通常是由一個或一組進程來實現(xiàn)的,它本身也和用戶程序一樣占用heap空間,運行時也占用CPU。當(dāng)GC進程運行時,應(yīng)用程序停止運行。如果GC運行時間較長時,用戶能夠感到Java程序的停頓(超時);如果GC運行時間太短,則可能對象回收率太低,這意味著還有很多應(yīng)該回收的對象沒有被回收,仍然占用大量內(nèi)存。因此在設(shè)計GC的時候,就必須在停頓時間和回收率之間進行權(quán)衡。

增量式GC:通過一定的回收算法,把一個長時間的中斷,劃分為很多個小的中斷,通過這種方式減少GC對用戶程序的影響。雖然增量式GC在整體性能上可能不如普通GC的效率高,但是它能夠減少程序的最長停頓時間。增量式GC的實現(xiàn)采用TrainGC算法,它的基本想法是:將堆中的所有對象按照創(chuàng)建和使用情況進行分組(分層),將使用頻繁高和具有相關(guān)性的對象放在一隊中,隨著程序的運行,不斷對組進行調(diào)整。當(dāng)GC運行時,它總是先回收最老的(最近很少訪問的)的對象,如果整組都為可回收對象,GC將整組回收。這樣,每次GC運行只回收一定比例的不可達對象,保證程序的順暢運行。

為什么要分代:分代的垃圾回收策略,是基于這樣一個事實:不同對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。在Java程序運行的過程中,會產(chǎn)生大量的對象,其中有些對象是與業(yè)務(wù)信息相關(guān),比如Http請求中的Session對象、線程、Socket連接,這類對象跟業(yè)務(wù)直接掛鉤,因此生命周期比較長。還有一些對象主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如String對象,由于其不變類的特性,系統(tǒng)會產(chǎn)生大量的這些對象,有些對象甚至只用一次即可回收。

試想,在不進行對象存活時間區(qū)分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要遍歷所有存活對象,但實際上,對于生命周期長的對象而言,這種遍歷是沒有效果的,因為可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收采用分治的思想,進行代的劃分,把不同生命周期的對象放在不同代上,不同代上采用最適合它的垃圾回收方式進行回收。

分代的好處:如果單純從JVM的功能考慮(用簡單粗暴的標(biāo)記-清理刪除垃圾對象),并不需要新生代,完全可以針對整個堆進行操作,但是每次GC都針對整個堆標(biāo)記清理回收對象太慢了。把堆劃分為新生代和老年代有2個好處:

簡化了新對象的分配(只在新生代分配內(nèi)存),可以更有效的清除不再需要的對象(死對象)。

在新生代中,GC可以快速標(biāo)記回收“死對象”,而不需要掃描整個堆中的存活一段時間的“老對象”。

什么情況下觸發(fā)垃圾回收:由于對象進行了分代處理,因此垃圾回收區(qū)域、時間也不一樣。GC有兩種類型:Scavenge GC和Full GC。Scavenge GC:當(dāng)新對象生成,并且在Eden申請空間失敗時,就會觸發(fā)Scavenge GC,對Eden區(qū)域進行GC,清除非存活對象,并且把尚且存活的對象移動到Survivor區(qū)。這種方式的GC是對年輕代的Eden區(qū)進行,不會影響到年老代。因為大部分對象都是從Eden區(qū)開始的,同時Eden區(qū)不會分配的很大,所以Eden區(qū)的GC會頻繁進行。一般在這里需要使用速度快、效率高的算法,使Eden區(qū)能盡快空閑出來。

Full GC:對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進行回收,所以比Scavenge GC要慢,因此應(yīng)該盡可能減少Full GC的次數(shù)。在對JVM調(diào)優(yōu)的過程中,很大一部分工作就是對于FullGC的調(diào)節(jié)。有如下原因可能導(dǎo)致Full GC:

舊生代空間不足

持久代空間不足

CMS GC時出現(xiàn)了promotion failed和concurrent mode failure

統(tǒng)計得到新生代minor gc時晉升到舊生代的平均大小小于舊生代剩余空間

直接調(diào)用System.gc,可以DisableExplicitGC來禁止

分配擔(dān)保:老年代的對象中,有一小部分是因為在新生代回收時,老年代做擔(dān)保,進來的對象;絕大部分對象是因為很多次GC都沒有被回收掉而進入老年代。

新生代GC

一句話總結(jié):對象開始是創(chuàng)建在Eden區(qū),然后經(jīng)過在Survivor區(qū)域上的數(shù)次轉(zhuǎn)移而存活下來的長壽對象最后會被移到老年代。

Eden區(qū)

當(dāng)進行Eden區(qū)的回收時,垃圾回收器會從根對象開始遍歷所有的可達對象,并將它們標(biāo)記為存活狀態(tài)。

標(biāo)記完成后,所有存活對象會被復(fù)制到其中一個Survivor區(qū)。

于是整個Eden區(qū)便可認(rèn)為是清空了,又可以重新用來分配對象了。

這個過程叫做標(biāo)記復(fù)制(Mark and Copy):存活對象先被標(biāo)記,隨后被復(fù)制到Survivor區(qū)中。

Survivor區(qū)

緊挨著Eden的兩個Survivor區(qū)其中一個Survivor區(qū)始終都是空的。

空的Survivor區(qū)會在下一次新生代GC的時候迎來它的居民。

整個新生代中的所有存活對象(Eden和from區(qū))都會被復(fù)制到to區(qū)中。

一旦完成之后,對象便都跑到to區(qū)中,而Eden和from區(qū)則被清空了。

這時from區(qū)和to區(qū)兩者的角色便會發(fā)生調(diào)轉(zhuǎn)(下次GC時仍然從from到to)。

對象提升到老年代

存活對象會不斷地在兩個存活區(qū)之間來回地復(fù)制,直到其中的一些對象被認(rèn)為是已經(jīng)成熟,足夠老了。

在一輪GC完成后,每個分區(qū)中存活下來的對象的計數(shù)便會增加(如果剛剛從Eden存活下來其年齡=1),

當(dāng)一個對象的年齡超過了一個特定的年老閾值之后,它便會被提升到老年代中。

出現(xiàn)對象提升的時候,這些對象則不會再被復(fù)制到另一個存活區(qū),而是直接復(fù)制到老年代中。

如果存活區(qū)的大小不足以存放所有的新生代存活對象,則會出現(xiàn)過早提升。

步驟

1.對象分配

2.填充到Eden區(qū)

3.將Eden區(qū)中存活的對象(引用對象)拷貝到其中一個存活區(qū)

4.年齡計數(shù)器:在Eden中存活的對象其年齡初始=1,從其他存活區(qū)存活下來年齡+1

5.增加年齡計數(shù)器,圖中To存活區(qū)有三個對象來自于From存活區(qū),一個對象來自Eden

6.對象提升,這里假設(shè)年齡閾值=8,發(fā)生GC時,F(xiàn)rom存活區(qū)中=8的對象提升到老年代,其他存活對象移動到To存活區(qū)

7.總結(jié)下對象提升的過程:對象在新生代分配,每當(dāng)熬過一次YGC,對象的年齡計數(shù)器+1,當(dāng)達到閾值時仍然存活,提升到老年代

8.總結(jié)下GC過程:對象在新生代分配并填充,當(dāng)新生代滿時發(fā)生YGC,當(dāng)對象在存活區(qū)熬過一定年齡,提升到老年代

新生代GC的特點:

只要JVM無法為新創(chuàng)建的對象分配空間,就肯定會觸發(fā)新生代GC,比如Eden區(qū)滿了。因此對象創(chuàng)建得越頻繁,新生代GC肯定也更頻繁

一旦內(nèi)存池滿了,它的所有內(nèi)容就會被拷貝走,指針又將重新歸零。因此和經(jīng)典的標(biāo)記、清除、整理的過程不同的是

Eden區(qū)和Survivor區(qū)的清理只涉及到標(biāo)記和拷貝。在它們中是不會出現(xiàn)碎片的。寫指針始終在當(dāng)前使用區(qū)的頂部

在一次新生代GC事件中,通常不涉及到年老代。年老代到年輕代的引用被認(rèn)為是GC的根對象。而在標(biāo)記階段中,從年輕代到年老代的引用則會被忽略掉

所有的新生代GC都會觸發(fā)stop-the-world暫停,這會中斷應(yīng)用程序的線程。對絕大多數(shù)應(yīng)用而言,暫停的時間是可以忽略不計的

合理設(shè)置新生代大小

新生代過小,會導(dǎo)致新生對象很快就晉升,到老年代中,在老年代中對象很難被回收

新生代過大,會發(fā)生過多的復(fù)制過程

我們的目標(biāo)是:最小化短命對象晉升到老年代的數(shù)量,最小化新生代GC的次數(shù)和持續(xù)時間

JVM的新生代GC算法

Serial Copying:單CPU、新生代小、對暫停時間要求丌高的應(yīng)用

Parallel Scavenge:多CPU、對暫停時間要求較短的應(yīng)用

ParNew:Serial Copying的多線程版本

均使用復(fù)制算法,在分配對象時,如果Eden空間不足觸發(fā)新生代GC

JVM的新生代GC優(yōu)化

指針碰撞(bump-the-pointer):Bump-the-pointer技術(shù)會跟蹤在Eden上新創(chuàng)建的對象。由于新對象被分配在Eden空間的最上面,所以后續(xù)如果有新對象創(chuàng)建,只需要判斷新創(chuàng)建對象的大小是否滿足剩余的Eden空間。如果新對象滿足要求,則其會被分配到Eden空間,同樣位于Eden的最上面。所以當(dāng)有新對象創(chuàng)建時,只需要判斷此新對象的大小即可,因此具有更快的內(nèi)存分配速度。

然而,在多線程環(huán)境下,將會有別樣的狀況。為了滿足多個線程在Eden空間上創(chuàng)建對象時的線程安全,不可避免的會引入鎖,因此隨著鎖競爭的開銷,創(chuàng)建對象的性能也大打折扣。

線程局部分配緩沖區(qū)(Thread-Local Allocation Buffers)

在HotSpot中正是通過TLABs解決了多線程問題。TLABs允許每個線程在Eden上有自己的小片空間,線程只能訪問其自己的TLAB區(qū)域,因此bump-the-pointer能通過TLAB在不加鎖的情況下完成快速的內(nèi)存分配。

老年代GC

老年代GC算法:

Serial MSC/Serial Old/Serial Mark Sweep Compact

Parallel Compacting/Parallel Old

CMS

下圖是新生代GC和老年代GC的幾種組合方式:

新生代Serial+老年代Serial(Serial Old)

新生代ParNew+老年代Serial(Serial Old)

新生代Parallel Scavenge+老年代Parallel(Parallel Old)

CMS收集器:

CMS GC工作流程

CMS GC是針對老年代的GC算法,CMS采用標(biāo)記清理算法,只不過并不是嚴(yán)格意義的標(biāo)記清理

1.年輕代被分割成一個Eden區(qū)和兩個survivor區(qū),老年代是一片連續(xù)空間

2.你的JAVA程序運行一段時間后,對象分散地分布在老年代中,

使用CMS,老年代是對象消亡的地方,它們不會被移動,也不會被壓縮,除非遇到一個Full GC

3.年輕代GC工作過程:

① 存活的對象從Eden區(qū)或一個Survivor區(qū),移動到另外一個Survivor區(qū)

② 一些老的對象如果達到了晉升閾值,就會被提升到老年代中

4.年輕代GC后,Eden區(qū)和其中一個Survivor區(qū)會被清理,Survivor中存活對象如果超過晉升閾值,晉升到老年代

5.老年代GC采用標(biāo)記清理算法

6.老年代GC經(jīng)過清理階段后,很多內(nèi)存會被釋放,但是仍然沒有壓縮

CMS(Concurrent Mark Sweep)

CMS收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器(盡可能降低停頓),它是基于“標(biāo)記-清除”算法實現(xiàn)的,它的目標(biāo)是盡量減少應(yīng)用的暫停時間,減少Full GC發(fā)生的幾率,利用和應(yīng)用程序線程并發(fā)(允許垃圾回收線程和應(yīng)用線程共享處理器資源,比如下面的步驟2、3并發(fā)兩個階段)的垃圾回收線程來標(biāo)記清除年老代。整個收集過程大致分為4個步驟:

初始標(biāo)記(CMS-initial-mark):從根對象開始標(biāo)記直接關(guān)聯(lián)的對象,會產(chǎn)生全局停頓

并發(fā)標(biāo)記(CMS-concurrent-mark):進行GC Root根搜索算法階段,會判定對象是否存活

重新標(biāo)記(CMS-remark):由于并發(fā)標(biāo)記時,用戶線程依然運行,因此在正式清理前,再做修正。

這個階段的停頓時間會被初始標(biāo)記階段稍長,但比并發(fā)標(biāo)記階段要短

并發(fā)清除(CMS-concurrent-sweep):基于標(biāo)記結(jié)果,直接清理對象(清理的是沒有標(biāo)記的對象)

下圖結(jié)合了其他類型的收集器,對比CMS。可以看到CMS只不過是把一段較長的Stop the World暫停所有應(yīng)用程序,分成了兩個較短的暫停。第一次暫停(初始標(biāo)記)從GC Roots對象開始標(biāo)記存活的對象;第二次暫停(重新標(biāo)記)是在并發(fā)標(biāo)記之后,重新標(biāo)記并發(fā)標(biāo)記階段遺漏的對象(在并發(fā)標(biāo)記階段結(jié)束后對象狀態(tài)的更新導(dǎo)致)。第一次暫停會比較短,第二次暫停通常會比較長,并且重新標(biāo)記這個階段可以并行標(biāo)記(初始標(biāo)記使用一個線程,重新標(biāo)記使用多線程)。

CMS收集器的優(yōu)點:由于整個過程中耗時最長的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,所以整體來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。

CMS收集器的缺點:

1.與其他GC相比,CMS GC要求更多的內(nèi)存空間和CPU資源。在并發(fā)階段,雖然不會導(dǎo)致用戶線程停頓,但是會占用CPU資源而導(dǎo)致引用程序變慢,總吞吐量下降。CMS默認(rèn)啟動的回收線程數(shù)是:(CPU數(shù)量+3)/4,可以通過-XX:ParallelCMSThreads設(shè)定CMS的線程數(shù)量。由于GC線程與應(yīng)用搶占CPU,會影響系統(tǒng)整體吞吐量和性能:在用戶線程運行過程中,分一半CPU去做GC,系統(tǒng)性能在GC階段就下降一半。

2.清理不徹底,會產(chǎn)生浮動垃圾:因為在并發(fā)清理階段(步驟4),伴隨程序的運行自然會有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。

3.因為GC的同時應(yīng)用也在運行,不能在老年代空間快滿時再清理,Old區(qū)需要預(yù)留足夠的內(nèi)存空間給用戶線程使用:否則如果在并發(fā)GC期間(步驟4),用戶線程又申請了大量內(nèi)存(即使產(chǎn)生的是垃圾對象,也沒辦法在本次清理),導(dǎo)致內(nèi)存不夠。如果預(yù)留的空間不夠,就會出現(xiàn)Concurrent Mode Failure的錯誤。

因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預(yù)留一部分內(nèi)存空間提供并發(fā)收集時的程序運作使用。在默認(rèn)設(shè)置下,CMS收集器在老年代使用了68%的空間時就會被激活,也可以通過參數(shù)-XX:CMSInitiatingOccupancyFraction的值來提供觸發(fā)百分比,以降低內(nèi)存回收次數(shù)提高性能。要是CMS運行期間預(yù)留的內(nèi)存無法滿足程序其他線程需要,就會出現(xiàn)”Concurrent Mode Failure”失敗,這時候虛擬機將啟動后備預(yù)案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數(shù)-XX:CMSInitiatingOccupancyFraction設(shè)置的過高將會很容易導(dǎo)致”Concurrent Mode Failure”失敗,性能反而降低。

假設(shè)Old GC時,老年代總內(nèi)存=10G,占用了9G(由于碎片存在,不足分配新對象,發(fā)生Old GC)

在并行清理階段,即使把垃圾都清理完,釋放了3G,現(xiàn)在占用9-3=6G,但是如果在并發(fā)清理階段

新產(chǎn)生的對象占用了4G,本次垃圾無法清理,導(dǎo)致內(nèi)存占用=6+4=10G,老年代空間又滿了!

所以對于老年代的GC應(yīng)該預(yù)留一定的內(nèi)存空間給并發(fā)清理階段產(chǎn)生的對象,默認(rèn)值是68%。

假設(shè)老年代總內(nèi)存=10G,當(dāng)使用了6.8G時,就會觸發(fā)老年代GC,而不是等到差不多占滿才觸發(fā)。

舉例使用6.8G后,釋放了3G,現(xiàn)在占用6.8-3=3.8G,即使新產(chǎn)生了4G不會在本次垃圾收集被清理

總的內(nèi)存占用也指頭3.8+4=7.8G,雖然又會觸發(fā)一次老年代GC,但是不至于把老年代的內(nèi)存用光。

4.CMS GC默認(rèn)不提供內(nèi)存壓縮,為了避免過多的內(nèi)存碎片而需要執(zhí)行壓縮任務(wù)時,CMS GC會比任何其他GC帶來更多的stop-the-world時間:因為整理過程是獨占的,會引起停頓時間變長。不過CMS允許設(shè)置進行幾次Full GC后,進行一次碎片整理。

CMS是基于“標(biāo)記-清除”算法實現(xiàn)的收集器,使用“標(biāo)記-清除”算法收集后,會產(chǎn)生大量碎片。空間碎片太多時,將會給對象分配帶來很多麻煩,比如說大對象,內(nèi)存空間找不到連續(xù)的空間來分配不得不提前觸發(fā)一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關(guān)參數(shù),用于在Full GC之后增加一個碎片整理過程,還可通過-XX:CMSFullGCBeforeCompaction參數(shù)設(shè)置執(zhí)行多少次不壓縮的Full GC之后,跟著來一次碎片整理過程。

Q&A

為什么CMS要使用標(biāo)記清除而不是標(biāo)記壓縮?:如果使用標(biāo)記壓縮,需要對(存活)對象的內(nèi)存位置進行改變,這樣程序就很難繼續(xù)執(zhí)行

CMS采用標(biāo)記清除有什么缺點?:標(biāo)記清除會產(chǎn)生大量內(nèi)存碎片,不利于內(nèi)存分配

CMS有沒有暫停?:CMS并非沒有暫停,而是用兩次短暫停來替代串行標(biāo)記整理算法的長暫停

CMS中的C(Concurrent)并發(fā)什么意思:并發(fā)標(biāo)記、并發(fā)清除、并發(fā)重設(shè)階段的所謂并發(fā),是指一個或者多個垃圾回收線程和應(yīng)用程序線程并發(fā)地運行,垃圾回收線程不會暫停應(yīng)用程序的執(zhí)行,如果你有多于一個處理器,那么并發(fā)收集線程將與應(yīng)用線程在不同的處理器上運行,顯然,這樣的開銷就是會降低應(yīng)用的吞吐量。Remark階段的并行,是指暫停了所有應(yīng)用程序后,啟動一定數(shù)目的垃圾回收進程進行并行標(biāo)記,此時的應(yīng)用線程是暫停的。

CMS GC日志

1

2

3

4

5

6

7

8

9

10

1.662: [GC [1CMS-initial-mark:28122K(49152K)]29959K(63936K),0.0046877secs] [Times: user=0.00sys=0.00, real=0.00secs]

1.666: [CMS-concurrent-mark-start]

1.699: [CMS-concurrent-mark:0.033/0.033secs] [Times: user=0.25sys=0.00, real=0.03secs]

1.699: [CMS-concurrent-preclean-start]

1.700: [CMS-concurrent-preclean:0.000/0.000secs] [Times: user=0.00sys=0.00, real=0.00secs]

1.700: [GC[YG occupancy:1837K (14784K)]1.700: [Rescan (parallel) ,0.0009330secs]1.701: [weak refs processing,0.0000180secs] [1CMS-remark:28122K(49152K)]29959K(63936K),0.0010248secs] [Times: user=0.00sys=0.00, real=0.00secs]

1.702: [CMS-concurrent-sweep-start]

1.739: [CMS-concurrent-sweep:0.035/0.037secs] [Times: user=0.11sys=0.02, real=0.05secs]

1.739: [CMS-concurrent-reset-start]

1.741: [CMS-concurrent-reset:0.001/0.001secs] [Times: user=0.00sys=0.00, real=0.00secs]

G1 GC

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 原文閱讀 前言 這段時間懈怠了,罪過! 最近看到有同事也開始用上了微信公眾號寫博客了,挺好的~給他們點贊,這博客我...
    碼農(nóng)戲碼閱讀 6,018評論 2 31
  • Java 虛擬機有自己完善的硬件架構(gòu), 如處理器、堆棧、寄存器等,還具有相應(yīng)的指令系統(tǒng)。JVM 屏蔽了與具體操作系...
    尹小凱閱讀 1,706評論 0 10
  • 轉(zhuǎn)載blog.csdn.net/ning109314/article/details/10411495/ JVM工...
    forever_smile閱讀 5,400評論 1 56
  • 1.一些概念 1.1.數(shù)據(jù)類型 Java虛擬機中,數(shù)據(jù)類型可以分為兩類:基本類型和引用類型。基本類型的變量保存原始...
    落落落落大大方方閱讀 4,572評論 4 86
  • 作者:一字馬胡 轉(zhuǎn)載標(biāo)志 【2017-11-12】 更新日志 日期更新內(nèi)容備注 2017-11-12新建文章初版 ...
    beneke閱讀 2,232評論 0 7