前言
閱讀過王子之前JVM文章的小伙伴們,應該已經(jīng)對JVM的內(nèi)存分布情況有了一個清晰的認識了,今天我們就接著來聊聊JVM的垃圾回收機制,讓小伙伴們輕松理解JVM是怎么進行垃圾回收的。
復制算法、Eden區(qū)和Survivor區(qū)
首先我們就來探索一下對于JVM堆內(nèi)存中的新生代區(qū)域,是怎么進行垃圾回收的。
實際上JVM是把新生代分為三塊區(qū)域的:1個Eden區(qū),2個Survivor區(qū)。
其中Eden區(qū)占用80%的內(nèi)存空間,每塊Survivor各占用10%的內(nèi)存空間。比如Eden區(qū)有800M,那么每個Survivor區(qū)就有100M。
平時可以使用的區(qū)域是Eden區(qū)和其中一塊Survivor區(qū),也就是900M的內(nèi)存空間。

剛開始創(chuàng)建對象的時候,對象都是分配在Eden區(qū)中的,如果Eden區(qū)快滿了,就會觸發(fā)垃圾回收 Young GC,使用的就是復制算法進行垃圾回收,流程如下:
首先會把Eden區(qū)中的存活對象一次性轉(zhuǎn)入其中一塊空著的Survivor區(qū)中。
然后清空Eden區(qū),之后新創(chuàng)建的對象就再次被放入了Eden區(qū)中了。
如果下次Eden區(qū)快滿了,就會再次觸發(fā)Young GC,這個時候會把Eden區(qū)和存在對象的Survivor區(qū)中存活的對象轉(zhuǎn)移到另一塊空著的Survivor區(qū)中,并清空Eden區(qū)和之前存在對象的Survivor區(qū)。
這就是復制算法的流程。
一直要保持一個Survivor區(qū)是空的以供復制算法垃圾回收,而這塊區(qū)域只占用整個內(nèi)存的10%,其他90%的內(nèi)存都能被使用,可見內(nèi)存利用率還是相當高的。
什么時候進入老年代
接下來我們就來看一下什么時候會進入老年代,這個問題上篇文章輕松理解JVM的分代模型中已經(jīng)簡單的介紹過了,今天會對此展開進行詳細探索。
1.躲過15次GC后進入老年代
在默認的情況下,如果新生代中的某個對象經(jīng)歷了15次GC后,還是沒有被回收掉,那么它就會被轉(zhuǎn)入到老年代中。
這個具體躲過多少次,是可以自己設置的,通過JVM參數(shù)“-XX:MaxTenuringThreshold”來設置,默認是15.
2.動態(tài)對象年齡判斷
另一種判斷方式也可以進入老年代,是不用等待GC15次的。
它的大致規(guī)則是,假如一批對象總大小大于了當前Survivor區(qū)域內(nèi)存的大小的50%,那么大于等于這批對象年齡的對象就會被轉(zhuǎn)移到老年代。
小伙伴們可能覺得有些沒看明白這句話的意思,沒關系,我們看一下圖

假設Survivor中有兩個對象,它們都經(jīng)歷過2次GC,年齡是2歲,而且兩個對象加在一起的大小大于50M,也就是超過了Survivor區(qū)域內(nèi)存大小的50%,那么這個時候,Survivor區(qū)域中年齡大于等于2歲的對象就要全部轉(zhuǎn)移到老年代中。
這就是所謂的動態(tài)年齡判斷規(guī)則。
要注意的是,年齡1+年齡2+年齡n的多個年齡對象大小超過Survivor區(qū)的50%,此時會把年齡n以上的對象放入老年代。
3.大對象直接進入老年代
有一個JVM參數(shù)"-XX:PretenureSizeThreshold",默認值是0,表示任何情況都先把對象分配給Eden區(qū)。
我們可以給他設置一個字節(jié)數(shù)1048576字節(jié),也就是1M。
它的意思就是當要創(chuàng)建的對象大于1M的時候,就會直接把這個對象放入到老年代中,壓根不會經(jīng)過新生代。
因為大對象在經(jīng)歷復制算法進行GC的時候是會降低性能的,所以直接放入老年代就可以了。
4.Young GC后存活的對象太多無法放入Survivor區(qū)
還有一種情況,就是Young GC后存活的對象太多,Survivor區(qū)放不下了,這個時候就會把這些對象直接轉(zhuǎn)移到老年代中。
這里我們就要思考一個問題了,如果老年代也放不下了怎么辦呢?
老年代空間分配擔保原則
首先,在執(zhí)行任何一次Young GC之前,JVM都會先檢查一下老年代可用的內(nèi)存空間是否大于新生代所有對象的總大小。
為啥要檢查這個呢?因為在極端情況下,Young GC后,新生代中所有的對象都存活下來了,那就會把所有新生代中的對象放入老年代中。
如果說老年代可用內(nèi)存大于新生代對象總大小,那么就可以放心的執(zhí)行Young GC了。
但是如果老年代的可用內(nèi)存小于新生代對象的總大小,這個時候就會看一個參數(shù)“-XX:HandlePromotionFailure”是否設置為true了(可以認為jdk7之后,默認設置為true)。
如果設置為true,那么進入下一步判斷,就是看看老年代可用的內(nèi)存,是否大于之前每次Young GC后進入老年代對象的平均大小。
如果說老年代的可用內(nèi)存小于平均大小,或者說參數(shù)沒有設置成true,那么就會直接觸發(fā)“Full GC”,就是對老年代進行垃圾回收,騰出空間后,再進行Young GC。
如果上邊兩種情況判斷成功,沒有執(zhí)行Full GC,進行了Young GC,有以下幾種可能:
1.如果Young GC后,存活的對象大小小于Survivor區(qū)域的大小,那么直接進入Survivor區(qū)域即可。
2.如果Young GC后,存活的對象大小大于Survivor區(qū)域的大小,但是小于老年代可用內(nèi)存大小,那就直接進入老年代。
3.很不幸,老年代可用空間也放不下這些存活對象了,那就會發(fā)生“Handle Promotion Failure”的情況,觸發(fā)Full GC。
如果Full GC后,老年代可用內(nèi)存還是不夠,那么就會導致OOM內(nèi)存溢出了。
這段內(nèi)容可能比較繁瑣,結(jié)合內(nèi)存模型,多看兩遍相信小伙伴們是可以讀懂的。
老年代的垃圾回收算法
接下來我們就來介紹一下老年代的垃圾回收算法,標記整理算法,理解起來還是比較容易的。

開始時我們的對象是胡亂分布的,經(jīng)過垃圾回收后,會標記出哪些是存活對象,哪些是垃圾對象,而后會把這些存活對象在內(nèi)存中進行整理移動,盡量都挪到一邊去靠在一起,然后再把垃圾對象進行清除,這樣做的好處就是避免了垃圾回收后產(chǎn)生大片的內(nèi)存碎片。
但是這一過程其實是比較耗時的,至少要比新生代的垃圾回收算法慢10倍。
所以如果系統(tǒng)頻繁出現(xiàn)Full GC,會嚴重影響系統(tǒng)性能,出現(xiàn)頻繁卡頓。
所以JVM優(yōu)化的一大問題就是減少Full GC頻率。
垃圾回收器
新生代和老年代進行垃圾回收的時候是通過不同的垃圾回收器進行回收的。
Seral和Seral Old垃圾回收器:分別用于回收新生代和老年代。
工作原理是單線程運行,垃圾回收的時候會停止我們系統(tǒng)的其他線程,讓系統(tǒng)卡死不動,然后執(zhí)行垃圾回收,這個現(xiàn)在基本已經(jīng)不會使用了
ParNew和CMS垃圾回收器:分別用于回收新生代和老年代。
它們都是多線程并發(fā)的,性能更好,現(xiàn)在一般是線上生產(chǎn)系統(tǒng)的標配。
G1垃圾回收器:統(tǒng)一收集新生代和老年代,采用了更加優(yōu)秀的算法機制。
這里只是給大家做一下簡單的介紹,更詳細的內(nèi)容以后文章會單獨解析。
Stop the World
JVM最大的痛點就是Stop the World了。
在垃圾回收的時候,盡可能的要讓垃圾回收器專心的去做垃圾回收的操作(防止垃圾回收的時候還在創(chuàng)建新對象,那不就亂套了嗎),所以此時JVM會在后臺進入Stop the World狀態(tài)。
進入這個狀態(tài)后,會直接停止我們系統(tǒng)的工作線程,讓我們的代碼不在運行。
接著垃圾回收完成后,會恢復工作線程,代碼就可以繼續(xù)運行了。
所以說只要是經(jīng)歷GC,其實就會讓系統(tǒng)卡死一段時間,新生代的垃圾回收可能感受不到太多,單老年代的垃圾回收耗時更多,可能會明顯的感覺到系統(tǒng)的卡死。
所以說無論是新生代的垃圾回收還是老年代的垃圾回收,我們都應該盡量的減少它們的頻率。
總結(jié)
今天的干貨內(nèi)容還是比較多的,相信小伙伴們閱讀后對JVM會有一個更深的了解。
建議小伙伴們自己找資料了解一下幾種垃圾回收器的實現(xiàn)原理,我們之后的文章會陸續(xù)介紹。
好了,那就到這里了,歡迎評論區(qū)留言討論。你的支持就是我更新的動力!
往期文章推薦: