作者簡介:大家好,我是smart哥,前中興通訊、美團(tuán)架構(gòu)師,現(xiàn)某互聯(lián)網(wǎng)公司CTO
聯(lián)系qq:184480602,加我進(jìn)群,大家一起學(xué)習(xí),一起進(jìn)步,一起對抗互聯(lián)網(wǎng)寒冬
學(xué)習(xí)必須往深處挖,挖的越深,基礎(chǔ)越扎實!
階段1、深入多線程
階段2、深入多線程設(shè)計模式
階段3、深入juc源碼解析
階段4、深入jdk其余源碼解析
階段5、深入jvm源碼解析
一、簡介
我們在前兩章中,已經(jīng)講解了JVM垃圾回收的基本流程和對象存活判定的算法,但是,并沒有深入垃圾回收內(nèi)部的細(xì)節(jié)。本章,我們就深入垃圾回收的內(nèi)部,看看JVM到底是如何進(jìn)行對象內(nèi)存的回收的。
二、復(fù)制算法
復(fù)制算法,主要用于?新生代?中對象的回收。其基本思路就是:將新生代內(nèi)存按劃分為大小相等的兩塊,每次只使用其中的一塊,當(dāng)一塊內(nèi)存用完了,?將存活的對象移動到另外一塊上面?,然后在把已使用過的內(nèi)存空間一次清理掉。
2.1 算法流程
我們以示例代碼來看復(fù)制算法的執(zhí)行流程:
public class Kafka {
public static void main(String[] args) throws InterruptedException {
loadReplicaFromDisk();
}
private static void loadReplicaFromDisk() {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.load();
}
}
假設(shè)程序執(zhí)行到replicaManager.load(),JVM的內(nèi)存數(shù)據(jù)結(jié)構(gòu)如下,其中“大量垃圾對象無人引用”表示其它程序產(chǎn)生的垃圾對象,此時新生代中用于分配對象內(nèi)存的區(qū)域也快滿了,再次為對象分配內(nèi)存時就會觸發(fā)“Minor GC”:
此時,一種最基本的思路就是,首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。但是這種方法問題也很明顯:?產(chǎn)生大量內(nèi)存碎片,導(dǎo)致后續(xù)需要為大對象分配空間時內(nèi)不足?。所以,JVM很少用這種方法,而是先將存活對象轉(zhuǎn)移到另一塊區(qū)域:
然后一次性把原來那塊內(nèi)存清空:
上述整個流程,就是所謂的復(fù)制算法:?把新生代劃分為兩塊內(nèi)存區(qū)域,只使用其中一塊,當(dāng)這塊快滿的時候,把存活對象一次性轉(zhuǎn)移到另一塊,保證沒有內(nèi)存碎片,然后清空原來那塊,依次循環(huán)往復(fù)?。
2.2 算法優(yōu)化
上述復(fù)制算法的缺點很明顯:即?對內(nèi)存的使用效率太低?。比如我們給新生代分配了1G內(nèi)存,那其實只有512MB是實際使用的,很浪費內(nèi)存空間。那么如何來優(yōu)化呢?
我們回顧下?新生代中對象的特點:朝生暮死,也就是說新生代的絕大多數(shù)對象在經(jīng)歷1次GC后就會被回收掉,存活率非常低。
根據(jù)這個特點,?HotSpot VM?采用了一種做法,把新生代區(qū)域劃分成了三塊:?1個Eden區(qū)(80%),2個Survivor區(qū)(各占10%)?,最開始,對象只在Eden進(jìn)行分配:
如果Eden區(qū)快滿了,此時觸發(fā)GC會將Eden區(qū)中的存活對象轉(zhuǎn)移到其中一塊Survivor中,同時清空Eden:
下一次再分配空間時,依然在Eden區(qū)分配,然后觸發(fā)GC,將Eden的存活對象和上一次使用的Survivor中的存活對象轉(zhuǎn)移到另一塊空白Survivor中,然后清空Eden和使用過的Survivor,循環(huán)往復(fù)。
這種內(nèi)存劃分方式的最大好處就是只有10%的空間是閑置的,無論是垃圾回收的性能、內(nèi)存碎片的控制、內(nèi)存使用率,都非常好。
三、標(biāo)記整理算法
通過前一節(jié),大家應(yīng)該已經(jīng)了解了新生代的垃圾回收算法,本節(jié)我們就來看下老年代的垃圾回收算法——標(biāo)記整理算法。
3.1 何時進(jìn)入老年代
這里有一個問題,新生代的對象什么時候會進(jìn)入老年代?先給出一個結(jié)論,一共有五種情況:
- 新生代對象的年齡超過一定閾值(默認(rèn)15);
- 動態(tài)年齡判斷
- 大對象直接分配
- Survivor區(qū)空間不足
年齡閾值
之前我們提到過,新生代中的對象每逃過一輪GC,年齡都會加1,到年齡達(dá)到15時(也可以通過JVM參數(shù)-XX:MaxTenuring Threshold
設(shè)置),就會被轉(zhuǎn)移到老年代:
動態(tài)年齡判斷
動態(tài)對象年齡判斷的規(guī)則是: Survivor區(qū)的存活對象年齡從小到大進(jìn)行累加,當(dāng)累加到 X 年齡時的總和大于 Survivor區(qū)空間的50%時,那么比X大的對象都會晉升到老年代。
舉個例子,比如當(dāng)前Survivor區(qū)的分布如下,累加結(jié)果45%小于50%:
此時新生代GC后,有6%的對象進(jìn)入Survivor區(qū),則Survivor區(qū)分布如下圖:
這時從1歲加到4歲的對象總和51% 大于50%,但此時沒有大于四歲的對象,即沒有對象晉升 。此時再經(jīng)過一次新生代GC后,又有40%的對象進(jìn)入Survivor區(qū),Survivor區(qū)分布如下圖:
Survivor區(qū)的對象年齡從小到大進(jìn)行累加,當(dāng)累加到 3 年齡時的總和大于50%,那么比3大的都會晉升到老年代,即4歲的20%、5歲的20%晉升到老年代。
可以使用-XX:TargetSurvivorRatio來設(shè)置Survivor區(qū)空間的百分比,默認(rèn)值是50
大對象
對于一些大對象,JVM會直接將其分配到老年代。通過參數(shù)-XX:PretenureSizeThrehold,可以設(shè)置閾值,單位為字節(jié)。
JVM之所以要這么做,是為了避免新生代中出現(xiàn)屢次逃過GC的大對象,大對象在新生代的Eden和Survivor區(qū)的來回復(fù)制開銷比較大。
Survivor區(qū)空間不足
最后一種情況就是,Minor GC之后發(fā)現(xiàn)存活對象太多,沒法放入另一塊Survivor區(qū)域中,比如下面這種情況:
這時候就必須把這些對象全部遷移到老年代去:
3.2 空間分配擔(dān)保
前面討論了新生代的存活對象何時會轉(zhuǎn)移到老年代,那么問題又來了,如果老年代區(qū)域的內(nèi)存空間不足了怎么辦?這里就涉及了?空間分配擔(dān)保機(jī)制?。
所謂空間分配擔(dān)保,指在執(zhí)行任何一次Minor GC之前,JVM會檢查?老年代的最大連續(xù)可用空間?是否大于?新生代所有對象的總大小?:
如果大于,說明這次Minor GC肯定是安全的,因為老年代可以容納新生代中的所有對象;
如果小于,則 JVM 會查看-XX:HandlePromotionFailure
參數(shù)值,這個參數(shù)值表示是否允許擔(dān)保失?。?/p>
- 如果允許(
HandlePromotionFailure==true
),則看下?老年代的最大連續(xù)可用空間?是否大于?歷次Minor GC后進(jìn)入老年代的對象平均大小?。如果大于,就進(jìn)行minior GC,如果這次Minior GC失敗了,就會進(jìn)行FULL GC(所謂FULL GC,就是既對老年代進(jìn)行垃圾回收,也對新生代進(jìn)行垃圾回收);如果小于,先進(jìn)行FULL GC,再Minor GC。 - 如果不允許(
HandlePromotionFailure==false
),則直接觸發(fā)FULL GC,然后再進(jìn)行一次Minor GC。
如果經(jīng)過上面的操作,老年代可用空間最后發(fā)現(xiàn)還是不夠,就會導(dǎo)致所謂的OOM內(nèi)存溢出了。
總之,空間分配擔(dān)保機(jī)制的核心目的就是?避免頻繁FULL GC,能先預(yù)判就先預(yù)判?,實在不行才FULL GC,因為FULL GC的開銷非常大,既要對老年代進(jìn)行回收,也要對新生代進(jìn)行回收。
3.3 算法流程
了解了新生代對象何時進(jìn)入老年代,以及FULL GC的觸發(fā)時機(jī),我們就可以來看下老年代的?標(biāo)記整理算法?的流程了。標(biāo)記整理算法,其實就是先標(biāo)記存活對象,然后將存活對象都向內(nèi)存端邊界移動,然后清理掉端邊界以外的內(nèi)存,這樣就可以避免出現(xiàn)大量內(nèi)存碎片。
我們通過示例來看下,假設(shè)JVM當(dāng)前的內(nèi)存狀態(tài)如下,老年代中散落著各種存活對象:
接著,會將存活對象都往內(nèi)存的一邊移動,讓它們盡量緊湊,然后一次性把垃圾對象清理掉:
四、線上示例
通過前兩節(jié)的講解,相信讀者已經(jīng)對新生代的復(fù)制算法、老年代的標(biāo)記整理算法有所了解,本節(jié)我們將通過一個生產(chǎn)系統(tǒng)的GC案例,讓大家更加透徹的理解JVM中如果進(jìn)行對象分配和老年代轉(zhuǎn)移,以及Minor GC和Full GC的全過程。
4.1 背景
假設(shè)現(xiàn)在生產(chǎn)環(huán)境有一套“數(shù)據(jù)計算系統(tǒng)”,不停地從MySQL等各類數(shù)據(jù)源提取數(shù)據(jù)到內(nèi)存中進(jìn)行計算,系統(tǒng)是分布式的。每個節(jié)點(機(jī)器)每分鐘執(zhí)行100次操作(提取數(shù)據(jù)并計算,每次操作耗時10s),每次操作1萬條數(shù)據(jù),每條數(shù)據(jù)大小為1KB左右,那么每次數(shù)據(jù)的總大小就是10MB:
每臺機(jī)器的配置是4核8G,JVM分配4G內(nèi)存,其中新生代1.5G,老年代1.5G。
整個系統(tǒng)的初始背景大致就是上面這樣,下面來分析可能存在的各種問題。
4.2 頻繁Full GC
我們先來看下新生代的空間什么時候會被占滿,按照8:1:1來分配Eden和Survivor區(qū),如下圖:
每執(zhí)行一次操作,Eden區(qū)就會填充10MB數(shù)據(jù),一分鐘執(zhí)行100次操作就是1000MB,所以?Eden區(qū)基本上1分鐘左右就會被占滿?。再執(zhí)行操作時,就會進(jìn)行Minor以回收一部分的垃圾對象:
首先,檢查老年代的連續(xù)可用內(nèi)存空間是否足夠(即大于新生代中的所有存活對象大小),如下圖,老年代目前是空的,1.5G的可用內(nèi)存空間可以容納Eden區(qū)中的1.2G對象,所以會直接進(jìn)行Minor GC:
那么,?此時Eden區(qū)中有多少對象還是存活的呢??之前說了每次操作耗時10s,那么在1分鐘內(nèi)的最后10s時,前面0-50s的任務(wù)已經(jīng)執(zhí)行完了,1分鐘操作100個任務(wù),所以大約有1/6的任務(wù)還沒有執(zhí)行完畢,即大約還有20個任務(wù)在計算中(大約200MB對象存活):
其實線上一般是通過GC日志去分析存活對象的大小的,GC日志中清楚的記錄了每次Minor GC進(jìn)入到老年代的對象大?。ê竺嫖覀儠敿?xì)講解如何看懂GC日志),根據(jù)我們的線上日志分析,大約也還有200MB對象是存活的。
注意,每一塊Survivor區(qū)的大小只有100MB,所以是無法容納200MB的存活對象的,所以會通過空間擔(dān)保機(jī)制,轉(zhuǎn)移到老年代中,并清空Eden區(qū),此時JVM內(nèi)存空間結(jié)構(gòu)如下:
由于每分鐘老年代都被填充200MB存活對象,所以到第3分鐘結(jié)束時,老年代已經(jīng)有400MB空間被占滿,且Eden區(qū)也被占滿,此時如果要進(jìn)行Minor GC,會怎么樣呢?
首先,依然檢查老年代的連續(xù)可用內(nèi)存空間是否足夠(即大于新生代中的所有存活對象大?。?,此時發(fā)現(xiàn)空間是不夠的,老年代只有1.1GB可用,而新生代的所有對象大小有1.2GB。
此時,就會判斷是否開啟了空間擔(dān)保機(jī)制——即判斷HandlePromotionFailure
是否為true,如果開啟了(一般生產(chǎn)環(huán)境都會開啟),就會看下歷代晉升到老年代的對象大小是否小于老年代可用空間,根據(jù)之前的計算,歷代晉升到老年代的對象大小約為200MB,小于1.1GB,所以JVM就會放心的進(jìn)行一次Minor GC,此時又有200MB對象進(jìn)入到老年代。
重復(fù)上述過程,大約經(jīng)過8分鐘,經(jīng)歷7次Minor GC后,JVM內(nèi)存空間結(jié)構(gòu)如下,此時老年代剩余可用空間大約100MB,Eden區(qū)已被占滿:
此時又會進(jìn)行Minor GC前的檢查,但是老年代的可用空間已經(jīng)比歷代晉升到老年代的對象空間小了,所以會?直接觸發(fā)一次Full GC?,將老年代中的垃圾對象回收(假設(shè)此時老年代中的對象全部都可回收):
然后緊接著再進(jìn)行一次Minor GC,將Eden區(qū)中的200MB存活對象轉(zhuǎn)移到老年代:
按照上述這個模型,?基本上8分鐘左右就會觸發(fā)一次Full GC?,這個頻率對于生產(chǎn)環(huán)境是不可接受的,因為Full GC會嚴(yán)重影響系統(tǒng)性能,這個后面章節(jié)我們會詳細(xì)講解。
4.3 優(yōu)化
那么該如何進(jìn)行優(yōu)化呢?最基本的思路就是?增加Survivor的內(nèi)存大小?,因為正是Survivor區(qū)不能容納存活對象(200MB)導(dǎo)致必須晉升到老年代。所以重新分配新生代大小為2G,老年代為1G,同時改變Eden和Survivor的空間比例,這樣Survivor區(qū)就能容納每次Minor GC后的存活對象,如下圖:
比如經(jīng)過一段時間,JVM內(nèi)存結(jié)果如下,Eden區(qū)被占滿,Survivor1區(qū)有200MB上一輪Minor GC后的存活對象:
然后此時進(jìn)行Minor GC,會先清理到S1區(qū)中的所有對象,然后將Eden區(qū)中的存活對象(200MB)轉(zhuǎn)移到S2區(qū):
這樣,基本上就很少會有對象進(jìn)入到老年代,F(xiàn)ull GC的頻率能降低到幾小時一次。
五、總結(jié)
最后來總結(jié)下本章的內(nèi)容,本章主要介紹了新生代的復(fù)制算法和老年代的標(biāo)記整理算法的流程,重點需要掌握的是以下幾點:文章來源:http://www.zghlxwxcb.cn/news/detail-784039.html
- 新生代對象何時會進(jìn)入老年代?
- 何時會觸發(fā)新生代的Minor GC?
- 何時會觸發(fā)FULL GC?
- 空間分配擔(dān)保機(jī)制的作用是什么?
同時,本章也給出了一個線上示例,幫助讀者更好的理解JVM分代垃圾回收的整個流程。下一章開始,我們將詳細(xì)介紹各種垃圾回收器,看看它們內(nèi)部是如何運用GC算法進(jìn)行垃圾回收的。文章來源地址http://www.zghlxwxcb.cn/news/detail-784039.html
到了這里,關(guān)于JVM基礎(chǔ)(5)——JVM垃圾回收算法的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!