其實(shí),對(duì)于寫(xiě)代碼來(lái)說(shuō),也有垃圾回收這個(gè)問(wèn)題,這里所說(shuō)的垃圾,指的是程序中不再需要的內(nèi)存空間,垃圾回收指的是回收這些不再需要的內(nèi)存空間,讓程序可以重新利用這些釋放的內(nèi)存空間。
那么垃圾回收是怎么,是不采用算法來(lái)實(shí)現(xiàn)呢?本次課時(shí),我們就一起來(lái)探討 JAVA 的垃圾回收算法。
標(biāo)記-清除算法(Mark-Sweep)
標(biāo)記-清除,顧名思義,先標(biāo)記垃圾,再清除。它是垃圾回收最基礎(chǔ)的算法,后續(xù)很多算法都是基于它上面去改進(jìn)的。標(biāo)記-清除算法主要分成兩個(gè)階段:
標(biāo)記階段:需要回收的對(duì)象。那么這個(gè)過(guò)程其實(shí)就是使用可達(dá)性分析去判斷一個(gè)對(duì)象是不是垃圾的過(guò)程。
清除階段:標(biāo)記完成之后,就會(huì)統(tǒng)一清理掉要回收的對(duì)象。
用圖表示出來(lái)大致如下圖所示:

先去標(biāo)記哪些對(duì)象是存活的,哪些對(duì)象是可以回收的。標(biāo)記完成之后把它回收的對(duì)象直接刪掉。從這張圖可以看出來(lái)。標(biāo)記清除它存在一定的缺點(diǎn),標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致程序在運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。比如現(xiàn)在假設(shè)我們想在內(nèi)存里面分配一段連續(xù)三個(gè)單位的內(nèi)存空間,要分配一個(gè)超大的字節(jié)數(shù)組,在這樣的內(nèi)存結(jié)構(gòu)下就沒(méi)有辦法做到了。
標(biāo)記-整理算法(Mark-compact)
下面來(lái)看一下做標(biāo)記-整理算法,也有一些文章會(huì)把它翻譯成標(biāo)記-壓縮,本課程里面統(tǒng)一稱(chēng)作是標(biāo)記-整理算法,那么標(biāo)記-整理算法是怎玩的。
首先也是標(biāo)記要回收的對(duì)象,這個(gè)過(guò)程和標(biāo)記清除是一樣的,但是在標(biāo)記完成之后并不是直接清除掉要回收的對(duì)象,而是把所有的存活對(duì)象都?jí)嚎s到內(nèi)存的一端,最后在清理掉邊界之外的所有空間,所以不會(huì)產(chǎn)生內(nèi)存碎片,提高了內(nèi)存的利用率,這種算法適用于老年代。用圖表示出來(lái)大概如下圖所示:

先去標(biāo)記哪些對(duì)象是存活的,哪些對(duì)象可以回收,然后把存活的對(duì)象往內(nèi)存的一端壓縮,最后再把可以回收的對(duì)象除清除。這樣就可以避免內(nèi)存碎片的問(wèn)題,但是這種方式在標(biāo)記和整理移動(dòng)的過(guò)程中也是耗時(shí)的。
復(fù)制算法(Copy)
復(fù)制算法大致上是這樣玩的,把內(nèi)存分成兩塊,每次只使用其中的一塊,然后把正在使用的這塊內(nèi)存里面的存活對(duì)象復(fù)制到不使用的內(nèi)存里面去,然后再清理掉正在使用的這塊內(nèi)存里面的所有對(duì)象,接著交換兩塊內(nèi)存的角色,等待下一次回收。用圖表示大概如下圖所示:

把一塊內(nèi)存分成了兩塊,每次只使用其中的一塊,在做垃圾回收的時(shí)候,把存活的對(duì)象移動(dòng)到另外一端內(nèi)存里面去,然后清除掉這塊內(nèi)存里面的所有對(duì)象。那么在下一次回收的時(shí)候也是一樣,把存活的對(duì)象移動(dòng)回來(lái),然后清除掉這一塊內(nèi)存里面的所有對(duì)象。
三種算法對(duì)比
以上是三種比較基礎(chǔ)的垃圾回收算法,下面來(lái)對(duì)比一下這三種算法。
標(biāo)記-清除算法的優(yōu)點(diǎn):實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單。
缺點(diǎn):存在內(nèi)存碎片。另外使用標(biāo)記清除算法的時(shí)候,分配內(nèi)存的速度也會(huì)受到影響,這是為什么呢?你想,假設(shè)現(xiàn)在有一個(gè)比較大的對(duì)象,因?yàn)楝F(xiàn)在有很多碎片化的內(nèi)存空間。那么想找到一塊連續(xù)空間的話(huà),就需要去便利空閑鏈表,從而值查找哪一塊內(nèi)存可以存放這樣的對(duì)象。那么在極端場(chǎng)景下,需要把整個(gè)鏈表全部遍歷完,才能知道這個(gè)對(duì)象該分配到哪里去,又或者根本沒(méi)有辦法分配。那么遍歷空閑鏈表是需要時(shí)間的,所以分配內(nèi)存的速度會(huì)受到影響。
標(biāo)記-整理算法的優(yōu)點(diǎn):沒(méi)有碎片。
缺點(diǎn):整理存在開(kāi)銷(xiāo)。因?yàn)闃?biāo)記整理需要把對(duì)象集中起來(lái),放到內(nèi)存的一端,這個(gè)過(guò)程需要計(jì)算以及時(shí)間,并且對(duì)象越多,占的內(nèi)存越大,整理的開(kāi)銷(xiāo)也會(huì)越大。
復(fù)制算法的優(yōu)點(diǎn)是性能好,沒(méi)有碎片。一般來(lái)講復(fù)制算法比標(biāo)記-清除算法以及標(biāo)記-整理算法性能要好一些,因?yàn)樗幌駱?biāo)記-清除算法或者標(biāo)記-整理算法那樣,需要標(biāo)記哪些對(duì)象是存活的,哪些對(duì)象可以回收,而只需要找到存活的對(duì)象。然后移動(dòng)這個(gè)存活的對(duì)象。所以性能上會(huì)有一些優(yōu)勢(shì)。
缺點(diǎn):內(nèi)存使用率低。我們一次只能使用整個(gè)內(nèi)存的一半,內(nèi)存使用率最多只會(huì)達(dá)到 50%。
兩種綜合的算法
好,探討完三種基礎(chǔ)的垃圾回收算法之后,再來(lái)探討 Java 里面兩種相對(duì)綜合的垃圾回收算法。
第一種是分代收集算法。分代收集算法的思路是把一個(gè)內(nèi)存分成多個(gè)區(qū)域,不同的區(qū)域使用不同的回收算法去回收。代收集算法比較復(fù)雜,而且細(xì)節(jié)極其之多。我們將在下面詳細(xì)討論。
第二種是增量算法。你想,如果你的內(nèi)存非常的大,如果一次收集所有垃圾,那么需要耗費(fèi)的時(shí)間就會(huì)比較的長(zhǎng),有可能會(huì)造成系統(tǒng)長(zhǎng)時(shí)間的停頓。那么增量算法的思想是每次只收集一小片區(qū)域內(nèi)存空間的垃圾,這樣就可以減少系統(tǒng)的停頓。
分代收集算法
目前各種商業(yè)虛擬機(jī),它的堆內(nèi)存的回收基本上都采用了分代收集的方式。所以可想而知,分代收集算法有多么重要。
現(xiàn)在設(shè)計(jì)算法的思想是根據(jù)對(duì)象的存活周期,把內(nèi)存分成多個(gè)區(qū)域,然后不同的區(qū)域使用不同的垃圾回收算法去回收對(duì)象。Java 把堆分成了新生代和老年代。下圖前面探討 Java 內(nèi)存結(jié)構(gòu)的時(shí)候已經(jīng)詳細(xì)介紹過(guò)了。

那么經(jīng)過(guò)分代之后啊,垃圾回收可以分成以下幾類(lèi):
一是新生代的回收(Minor GC 或者 Young GC)。
二是老年代的回收(Major GC)。
三是整個(gè)堆的回收(Full GC)。
那么由于執(zhí)行 Major GC 的時(shí)候,一般也會(huì)伴隨著一次 Minor GC,所以可以認(rèn)為 Major GC ≈ Full GC 。那么很多時(shí)候,程序員在聊 Major GC 以及 Full GC 的時(shí)候,聊的就是一件事兒。
下面來(lái)看一下對(duì)象是怎么樣分配到堆內(nèi)存的,我們還是對(duì)照堆內(nèi)存的結(jié)構(gòu)去講解。對(duì)象在創(chuàng)建的時(shí)候會(huì)先存放到 Eden。當(dāng) Eden 滿(mǎn)了之后就會(huì)觸發(fā)垃圾回收,這個(gè)回收的過(guò)程是把 Eden 里面存活的對(duì)象拷貝的存活區(qū)里面的 From Survivor 或者是 To Survivor 里面去。
比如第一次回收 From Survivor 里面去了,那么下一次回收就會(huì)把 From Survivor 里面存活的對(duì)象拷貝到 To Survivor 里面去,再下一次就會(huì)把 From Survivor 面里面存活的對(duì)象拷貝的 From Survivor 里面去,周而復(fù)始。
不難發(fā)現(xiàn)這個(gè)回收的過(guò)程使用了復(fù)制算法,這也是為什么新生代要有兩個(gè) Survivor 的原因。因?yàn)閺?fù)制算法需要把一個(gè)內(nèi)存分成兩塊。那么對(duì)象每經(jīng)歷一次垃圾回收之后,如果還存活的話(huà),它的年齡就會(huì)增加 +1。當(dāng)對(duì)象的年齡達(dá)到閾值的話(huà),默認(rèn)情況下是 15,就會(huì)晉升到老年代。
老年代里面的對(duì)象存活率一般是比較高的,因?yàn)槟阆耄蓟厥?15 次了,還是沒(méi)能回收得了,所以后面繼續(xù)存活的可能性依然是比較大的。
那么老年代的對(duì)象一般會(huì)使用標(biāo)記-清除算法或者是標(biāo)記-整理算法去進(jìn)行回收。這里需要說(shuō)明一下,這個(gè)對(duì)象分配的過(guò)程只是一個(gè)典型的分配流程,實(shí)際情況是存在例外的:
一是,一個(gè)新建對(duì)象,并利率總是會(huì)分配到 Eden,也可能會(huì)直接進(jìn)入到老年代。主要有兩種場(chǎng)景。第一,如果你的對(duì)象大小,大于這個(gè)閾值就會(huì)直接分配到老年代。當(dāng)然了,默認(rèn)情況下這個(gè)參數(shù)的值是零,也就是說(shuō)不做限制,所有的對(duì)象都會(huì)優(yōu)先在 Eden 里面分配。第二,如果你的對(duì)象非常的大,比方說(shuō)一個(gè)超大的數(shù)組,新生代的空間根本都不夠,那么這個(gè)時(shí)候也會(huì)直接把這個(gè)對(duì)象放到老年代去擔(dān)保。之所以要允許對(duì)象直接分配到老年代,主要是因?yàn)樾律捎玫氖菑?fù)制算法,在 Eden 里面分配大對(duì)象的話(huà),將會(huì)導(dǎo)致 Eden 和兩個(gè) Survivor 區(qū)之間大量的內(nèi)存拷貝。
二是,對(duì)象也不一定要達(dá)到年齡閾值,才會(huì)進(jìn)入到老年代。虛擬機(jī)有一個(gè)動(dòng)態(tài)年齡的概念,就是說(shuō)如果存活區(qū)里面,所有相同年齡對(duì)象的大小的總和已經(jīng)大于 Survivor 空間的一半了。這個(gè)時(shí)候,如果某個(gè)對(duì)象的年齡大于這個(gè)年齡的話(huà),會(huì)直接進(jìn)入老年代,比如有一堆的對(duì)象,它的年齡值是 9,年齡都是 9 的所有對(duì)象它的總和以及大于 Survivor 空間的一半了,那么年齡大于 9 的對(duì)象就會(huì)直接進(jìn)入老年代。
觸發(fā)垃圾回收的條件
下面我們來(lái)看一下不同區(qū)域觸發(fā)垃圾回收的條件:
先來(lái)看一下新生代回收的觸發(fā)條件,Eden 空間不足就會(huì)進(jìn)行 Minor GC 回收新生代。
再來(lái)看一下 Full GC 的觸發(fā)條件,F(xiàn)ull GC 的觸發(fā)條件要復(fù)雜一些,主要有這么幾種場(chǎng)景:
一是,老年代空間不足,老年代代空間不足又分為兩種情況:第一是空間真的不足了。第二是內(nèi)存碎片,導(dǎo)致沒(méi)有連續(xù)的內(nèi)存去分配對(duì)象,觸發(fā) Full GC。
二是,元空間不足。
三是,在某一次新生代回收之后,要晉升到老年代的對(duì)象所占用的空間大于了老年代的剩余空間,這個(gè)時(shí)候也會(huì)觸發(fā) Full GG。
四是,顯式調(diào)用了 System.gc()。System.gc() 的作用是建議垃圾回收容器直徑垃圾回收,這個(gè)代碼是會(huì)觸發(fā) Full GC 的,你也可以使用這個(gè)參數(shù):-XX:DisableExplicitGC 去忽略掉 System.gc() 的調(diào)用。
好,介紹到這里可以簡(jiǎn)單總結(jié)一下,分代收集算法是根據(jù)對(duì)象的生命周期,把內(nèi)存做的分代。然后在分配對(duì)象的時(shí)候,把不同生命周期的對(duì)象放在不同代里面,不同的代上選用合適的回收算法進(jìn)行回收。
比如,新生代里面的對(duì)象存活周期一般都比較的短,每次垃圾回收的時(shí)候都會(huì)發(fā)現(xiàn)有大量的對(duì)象死去。那么 IBM 有做過(guò)研究,98% 以上的對(duì)象都是會(huì)很快消亡的,只有少量的對(duì)象能夠存活,所以新生代可以使用復(fù)制算法來(lái)完成垃圾收集。而老年代里面的對(duì)象存活率比較高,所以就采用標(biāo)記-清除算法或者是標(biāo)記-整理算法進(jìn)行回收。
那么相比單純的標(biāo)記-清除算法、標(biāo)記-整理算方法以及復(fù)制算法三代帶來(lái)了什么好處呢?
首先,分代可以更有效的清除不需要的對(duì)象,對(duì)于生命周期比較短的對(duì)象,對(duì)象還處于新生代的時(shí)候就會(huì)被回收掉了。
其次,分代提升的垃圾回收的效率。如果不做分代的話(huà),那么需要掃描整個(gè)堆里面的對(duì)象。而現(xiàn)在的話(huà)只要掃描新生代或者老年代就可以了。
總結(jié)
好,簡(jiǎn)單總結(jié)一下,本課時(shí)課我們探討了五種垃圾口的算法。基礎(chǔ)的垃圾回收算法有標(biāo)記-清除算法、標(biāo)記-整理以及復(fù)制算法。另外還探討了兩種綜合性的垃圾回收算法,即分代收集算法以及增量算法,同時(shí)詳細(xì)探討分代收集算法。
最后來(lái)總結(jié)一下分代收集算法的調(diào)優(yōu)原則:
第一,要合理的設(shè)置 Survivor 區(qū)的大小,因?yàn)?Survivor 區(qū)對(duì)內(nèi)存的利用率不高。如果配置的過(guò)大的話(huà),內(nèi)存浪費(fèi)就會(huì)比較嚴(yán)重。
第二,要讓 GC 盡量的發(fā)生在新生代,也就是讓 GC 停留在 Minor GC 的級(jí)別。
盡量減少 Full GC 的發(fā)生。
另外,總結(jié)有關(guān)堆內(nèi)存的 JVM 參數(shù)。
| 參數(shù) | 作用 | 默認(rèn) |
|---|---|---|
| -XX:NewRatio=n | 老年代:新生代內(nèi)存大小比值 | 2 |
| -XX:SurvivorRatio=n | 伊甸園:survivor區(qū)內(nèi)存大小比值 | 8 |
| -XX:PretenureSizeThreshold=n | 對(duì)象大小該值就在老年代分配,0表示不做限制 | 0 |
| -Xms | 需小堆內(nèi)存 | - |
| -Xmx | 最大堆內(nèi)存 | - |
| -Xmn | 新生代大小 | - |
| -XX:+DisableExplicitGC | 忽略掉 System.gc() 的調(diào)用 | 啟用 |
| -XX:NewSize=n | 新生代初始內(nèi)存大小 | - |
| -XX:MaxNewSize=n | 新生代最大內(nèi)存 | - |






