一文詳解,jvm內(nèi)存分代與垃圾回收原理
Java程序啟動后,本質(zhì)上就是啟動一個jvm進程,jvm會將自己管理的內(nèi)存劃分為幾個區(qū)域,每個區(qū)域都有自己的用途。在程序運行時的內(nèi)存區(qū)域主要可以劃分為五個,分別是:方法區(qū)、堆、虛擬機棧、本地方法棧、程序計數(shù)器??梢杂孟旅娴膱D來描述:圖1?jvm運行時數(shù)據(jù)區(qū)
jvm堆內(nèi)存分代
我們創(chuàng)建的對象,都會進入Java堆內(nèi)存中,堆內(nèi)存的分代模型分為:年輕代、老年代、永久代。
其實大部分對象的存活周期都是極短的,比如我們在方法里創(chuàng)建一個對象,離開這個方法后,這個對象的生命周期就結(jié)束了。一旦這個對象沒人使用了,就需要被jvm回收掉,釋放內(nèi)存空間。
少數(shù)對象是長期存活的,比如spring容器中創(chuàng)建的bean,一般都會一直存活在容器中。
每個對象的存活時間差別巨大,那么就需要差別化管理,就像城市規(guī)劃中,會劃分出生活區(qū),商業(yè)區(qū),制造業(yè)區(qū)。
于是jvm就有了分代模型,年輕代、老年代,jvm將堆內(nèi)存分為了這兩個區(qū)域。
年輕代,顧名思義,就是對象創(chuàng)建和使用完之后,很快就要回收的對象放在里面。
老年代呢,就是對象創(chuàng)建之后需要長期存在的對象放在里面,如下圖:圖2 jvm堆內(nèi)存分代
堆內(nèi)存分為年輕代、老年代,這跟垃圾回收有關(guān),對于年輕代里的對象,它們的特點是創(chuàng)建之后很快就會被回收,所以需要用一種垃圾回收算法。
對于老年代里的對象,他們的特點是需要長期存在,所以需要另外一種垃圾回收算法,所以需要分成兩個區(qū)域來放不同的對象。
對象的分配與流轉(zhuǎn)
一般來說,對象都是優(yōu)先分配在年輕代的。
public?class Server {
????private?static ConfigLoader loader = new ConfigLoader();
????public static void main(String[] args) {
????????loadLocalConfig();
????????while (true) {
???????? loadConfigFromRemote();
????????????Thread.sleep(1000);
????????}
????}
????
????private?static?void?loadLocalConfig() {
????????ConfigManager?configManager = new ConfigManager();
????????configManager.load();
????}
????private static void loadConfigFromRemote() {
????????loader.load();
????}
}
類靜態(tài)變量loader引用的那個ConfigLoader對象,首先分配在年輕代,是長期存活在內(nèi)存里的。loadLocalConfig方法中創(chuàng)建的configManager對象也是分配在新時代里的。?圖3 對象優(yōu)先分配在新生代什么時候觸發(fā)新生代的垃圾回收?當方法loadLocalConfig執(zhí)行完后,這個方法堆棧出棧,就沒有任何局部變量引用configManager對象了。
那么jvm就會立馬回收掉分配給它的內(nèi)存嗎?
不會的!如果新生代的內(nèi)存空間,幾乎都被全部對象給占滿了!此時假設(shè)我們代碼繼續(xù)運行,需要在新生代里去分配一個對象,發(fā)現(xiàn)內(nèi)存不夠了,這個時候就會觸發(fā)一次新生代的內(nèi)存回收。
新生代內(nèi)存空間的垃圾回收,也稱之為Minor GC或Young GC,它會嘗試把新生代里沒人引用的對象給回收掉。
對象是有年齡的,如果對象在一次垃圾回收后還存活,那么它的年齡就會加1。
默認情況下,如果成功躲過了15次垃圾回收,也就是15歲,還沒被回收掉,然后它會被轉(zhuǎn)移到Java堆內(nèi)存的老年代中去,顧名思義,老年代就是放這些年齡很大的對象。
什么情況下一個對象會被回收掉?
簡單來說一個對象不再被使用了,就會被垃圾回收掉,但jvm怎么判斷一個對象是否會繼續(xù)使用呢?
jvm使用了一種可達性分析的算法來判斷對象是不是可以被回收掉。
可達性分析法,是通過從GCRoots出發(fā),找出內(nèi)存中的引用鏈,那么鏈中的對象表示可達,即不能被垃圾回收。引用鏈之外的對象即可作為垃圾回收。
jvm中GC Roots有這幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI(即一般說的native方法)中引用的對象
其中,局部變量和類靜態(tài)變量比較常見。需要注意的是,類實例變量不是GC Roots。
年輕代的垃圾回收算法
新生代內(nèi)存區(qū)域劃分為三塊:1個Eden區(qū),2個Survivor區(qū),默認情況下Eden區(qū)占80%內(nèi)存空間,每一塊Survivor區(qū)各占10%內(nèi)存空間,
比如說新生代1G的內(nèi)存,Eden區(qū)就有800MB內(nèi)存,每一塊Survivor區(qū)就有100MB內(nèi)存,如下圖。
圖4 新生代內(nèi)存劃分
剛開始對象都是分配在Eden區(qū)內(nèi)的,如果Eden區(qū)快滿了,此時就會觸發(fā)垃圾回收。
新生代使用的復(fù)制算法,此時就會把Eden區(qū)中的存活對象都一次性轉(zhuǎn)移到一塊空著的Survivor區(qū)。survivor的中文意思是存活,顧名思義,就是放垃圾回收后存活的對象的。
接著Eden區(qū)就會被清空,然后再次分配新對象就會繼續(xù)分配到Eden區(qū)里,這樣只有Eden區(qū)和一塊Survivor區(qū)里是有對象的,其中Survivor區(qū)里放的是上一次Minor GC后存活的對象。新生代的垃圾回收就是這樣來回倒騰的。
所以1G內(nèi)存的新生代,平時可以使用的,就是Eden區(qū)和其中一塊Survivor區(qū),那么相當于有900MB的內(nèi)存是可以使用。
這么做最大的好處,就是只有10%的內(nèi)存空間是被閑置的,90%的內(nèi)存都被用上了。新生代對象存活期很短,存活的對象也很少,所以survivor分配的區(qū)域也比較小。
對象何時進入老年代?
對象一般優(yōu)先分配在年輕代,那么何時進入老年代呢?有這幾種情況:
- 躲過15次垃圾回收后
具體多少歲進入老年代,可以通過JVM參數(shù)-XX:MaxTenuringThreshold來設(shè)置,默認是15歲。
- 動態(tài)年齡判斷
他的大致規(guī)則就是,假如說當前存放對象的Survivor區(qū)域里,一批對象的總大小大于了這塊Survivor區(qū)域的內(nèi)存大小的50%,那么此時大于等于這批對象年齡的對象,就可以直接進入老年代了。
規(guī)則看起來比較晦澀,通俗理解就是:年齡1 年齡2 年齡n的多個年齡對象總和超過了Survivor區(qū)域的50%,此時就會把年齡n以上的對象都放入老年代。
- 大對象直接進入老年代
多大對象算大對象,可以通過JVM參數(shù)“-XX:PretenureSizeThreshold”來設(shè)置,可以把它的值設(shè)置為字節(jié)數(shù),比如“1048576”字節(jié),就是1MB。
之所以這么做,就是要避免新生代里出現(xiàn)那種大對象,屢次躲過GC,還得把他在兩個Survivor區(qū)域里來回復(fù)制多次之后才能進入老年代,
- Minor GC后對象太多Survivor放不下,把這些對象直接轉(zhuǎn)移到老年代去
有可能Eden區(qū)垃圾回收后,存活的對象太多,survivor放不下只能轉(zhuǎn)移到老年代去。
空間分配擔保機制
在發(fā)生Minor GC之前,JVM會先檢查一下老年代最大可用的連續(xù)內(nèi)存空間,是否大于新生代所有對象的總大小。
因為極端的情況下,可能新生代Minor GC過后,存活對象太多了,survivor放不下。
如果說發(fā)現(xiàn)老年代的內(nèi)存大小是大于新生代所有對象的,此時就可以放心大膽的對新生代發(fā)起一次Minor GC了,因為即使Minor GC之后所有對象都存活,Survivor區(qū)放不下了,也可以轉(zhuǎn)移到老年代去。
如果Minor GC之后新生代的對象全部存活下來,然后全部需要轉(zhuǎn)移到老年代去,但是老年代空間又不夠怎么辦?
那么就要看看老年代的內(nèi)存大小,是否大于之前每一次Minor GC后進入老年代的對象的平均大小。
如果老年代連續(xù)空閑空間大于新生代對象總大小,或者大于之前每一次Minor GC后進入老年代對象平均大小,就只觸發(fā)Minor GC,否則就要觸發(fā)Full GC。
假如此時進行Minor GC也有幾種可能:
(1)Minor GC過后,剩余的存活對象的大小,是小于Survivor區(qū)的大小的,那么此時存活對象進入Survivor區(qū)域即可。
(2)Minor GC過后,剩余的存活對象的大小,是大于 Survivor區(qū)域的大小,但是是小于老年代可用內(nèi)存大小的,此時就直接進入老年代即可。
(3)Minor GC過后,剩余的存活對象的大小,大于了Survivor區(qū)域的大小,也大于了老年代可用內(nèi)存的大小。此時老年代都放不下這些存活對象了,就會發(fā)生Handle Promotion Failure的情況,這個時候就會觸發(fā)一次Full GC。
如果要是Full GC過后,老年代還是沒有足夠的空間存放Minor GC過后的剩余存活對象,那么此時就會導致所謂的“OOM”內(nèi)存溢出異常了。
這段規(guī)則有點繞,所以必須畫個圖梳理下:
圖5?空間擔保機制
另外需要注意的是,上面描述的空間擔保機制是jdk6以后的,與java6之前稍稍不同。
總結(jié)下,老年代觸發(fā)垃圾回收的時機:
(1)Minor GC之前,發(fā)現(xiàn)很可能Minor GC之后要進入老年代的對象太多了,老年代放不下,此時需要提前觸發(fā)Full GC然后再帶著進行Minor GC;
(2)Minor GC之后,發(fā)現(xiàn)剩余對象太多,老年代都放不下了。
老年代回收,一般使用的標記整理算法,首先標記出來老年代當前存活的對象。
接著會讓這些存活對象在內(nèi)存里進行移動,把存活對象盡量都挪動到一起去,讓存活對象緊湊的靠在一起,避免垃圾回收過后出現(xiàn)過多的內(nèi)存碎片,然后再一次性把垃圾對象都回收掉。
總結(jié):
老年代存活對象比較多,存活對象比較大,所以老年代垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。
如果系統(tǒng)頻繁出現(xiàn)老年代的Full GC垃圾回收,會導致系統(tǒng)性能被嚴重影響,出現(xiàn)頻繁卡頓的情況。
所以所謂JVM優(yōu)化,就是盡可能讓對象都在新生代里分配和回收,盡量別讓太多對象頻繁進入老年代,避免頻繁對老年代進行垃圾回收,同時給系統(tǒng)充足的內(nèi)存大小,避免新生代頻繁的進行垃圾回收。