1.概述
提到垃圾回收,顧名思義,就是把已經(jīng)分配出去的,但卻不再使用的內(nèi)存回收回來(lái)。對(duì)于JVM來(lái)說(shuō),垃圾指的是在堆中死亡的對(duì)象所占據(jù)的內(nèi)存空間。
那么自然而然的,我們就能夠提出一個(gè)問(wèn)題:怎么知道對(duì)象死沒(méi)死?由這個(gè)問(wèn)題讓我們引出倆個(gè)比較有名的思路:
1.引用計(jì)數(shù)法
引用計(jì)數(shù)法是一個(gè)頗為古老的方式,原因它有致命的缺點(diǎn)。先不說(shuō)缺點(diǎn),咱們看一看它的思路。
它的做法是為每個(gè)對(duì)象添加一個(gè)引用計(jì)數(shù)器,用來(lái)統(tǒng)計(jì)指向該對(duì)象的引用個(gè)數(shù)。一旦某個(gè)對(duì)象的引用計(jì)數(shù)器為0,則說(shuō)明該對(duì)象已經(jīng)死亡,便可以被回收了。
它的具體實(shí)現(xiàn)是這樣子的:如果有一個(gè)引用,被賦值為某一對(duì)象,那么將該對(duì)象的引用計(jì)數(shù)器+1。如果一個(gè)指向某一對(duì)象的引用,被賦值為其他值,那么將該對(duì)象的引用計(jì)數(shù)器 -1。
對(duì)于引用計(jì)數(shù)法來(lái)說(shuō),除了需要額外的空間來(lái)存儲(chǔ)計(jì)數(shù)器,以及繁瑣的更新計(jì)數(shù)器以外;引用計(jì)數(shù)法還有一個(gè)重大的漏洞:無(wú)法處理循環(huán)引用。
假設(shè)對(duì)象 a 與 b 相互引用,除此之外沒(méi)有其他引用指向 a 或者 b。在這種情況下,a 和 b 實(shí)際上已經(jīng)死了,但由于它們的引用計(jì)數(shù)器皆不為 0,在引用計(jì)數(shù)法的心中,這兩個(gè)對(duì)象還活著。因此,這種情況下就造成了內(nèi)存泄露。

因?yàn)閙ain方法才是我們需要的對(duì)象,正在使用的,那么 A-B-C-D都是在使用的,但是 E-F-H三者互相引用,導(dǎo)致無(wú)法用標(biāo)記清除法回收。
所以目前 Java 虛擬機(jī)的主流垃圾回收器采取的是可達(dá)性分析算法。
2.可達(dá)性分析
2.1、基本概念
這種算法的思路在于:將一系列被稱為GC Roots的變量作為初始的存活對(duì)象合集,然后從該合集出發(fā),所有能夠被該集合引用到的對(duì)象,并將其加入到該集合中,而不能被該合集所引用到的對(duì)象,并可對(duì)其宣告死亡。
那么什么是 GC Roots 呢?
注意這句話:GC Roots是一些由堆外指向堆內(nèi)的引用,
一般而言,GC Roots 包括(但不限于)如下幾種:
Java 方法棧楨中的局部變量;已加載類的靜態(tài)變量;JNI handles;已啟動(dòng)且未停止的 Java 線程。可達(dá)性分析可以解決引用計(jì)數(shù)法所不能解決的循環(huán)引用問(wèn)題。舉例來(lái)說(shuō),即便對(duì)象 a 和 b 相互引用,只要從 GC Roots 出發(fā)無(wú)法到達(dá) a 或者 b,那么a和b就是死亡的對(duì)象。

如這個(gè)圖,網(wǎng)上的資料一般都沒(méi)說(shuō),根是什么?根就可以理解為main方法,你main方法就是根,你這這里new出來(lái)的對(duì)象,就是最開始的根,比如A。
根可達(dá)就是從mian方法往里面找,一直能找到最末尾的都是可達(dá)的,比如CD,但是 EFH三個(gè),從根是找不到的,這就是垃圾。
雖然可達(dá)性分析的算法本身很簡(jiǎn)明,但是在實(shí)踐中還是有不少其他問(wèn)題需要解決的。
2.2、多線程環(huán)境存在問(wèn)題
在多線程環(huán)境下,其他線程可能會(huì)更新已經(jīng)訪問(wèn)過(guò)的對(duì)象中的引用,而我們的可達(dá)性分析線程卻沒(méi)有同步到最新的內(nèi)容。那么就會(huì)造成誤報(bào)或者漏報(bào)。
對(duì)于JVM來(lái)說(shuō)漏報(bào)頂多損失了部分垃圾回收的機(jī)會(huì)。漏報(bào)則比較麻煩,因?yàn)槔厥掌骺赡芑厥樟巳员灰玫膶?duì)象…
怎么解決這個(gè)問(wèn)題呢?
2.2.1、Stop-the-world以及安全點(diǎn)
在 Java 虛擬機(jī)里,傳統(tǒng)的垃圾回收算法采用的是一種簡(jiǎn)單粗暴的方式,那便是 Stop-the-world,停止其他非垃圾回收線程的工作,直到完成垃圾回收。這也就造成了垃圾回收所謂的暫停時(shí)間(GC pause)。
Java 虛擬機(jī)中的 Stop-the-world 是通過(guò)安全點(diǎn)(safepoint)機(jī)制來(lái)實(shí)現(xiàn)的。當(dāng) Java 虛擬機(jī)收到 Stop-the-world 請(qǐng)求,它便會(huì)等待所有的線程都到達(dá)安全點(diǎn),才會(huì)停止所有線程,并允許請(qǐng)求Stop-the-world的那個(gè)線程進(jìn)行獨(dú)占的工作。
當(dāng)然也并非蠻橫的強(qiáng)制停止,畢竟多線程情況下,啥事都可能發(fā)生。安全點(diǎn)的初始目的并不是讓其他線程停下,而是找到一個(gè)穩(wěn)定的執(zhí)行狀態(tài)。在這個(gè)執(zhí)行狀態(tài)下,Java 虛擬機(jī)的堆棧不會(huì)發(fā)生變化。
3、垃圾回收的三種方式
通過(guò)上文我們聊到的可達(dá)性分析,我們可以對(duì)死亡對(duì)象宣判死刑。那么接下來(lái)我們便可以對(duì)死亡對(duì)象進(jìn)行回收工作了。主流的基礎(chǔ)回收方式可分為三種。
3.1、清除(sweep)
常見(jiàn)的一種叫法:標(biāo)記清除
思想:把死亡對(duì)象所占據(jù)的內(nèi)存標(biāo)記為空閑內(nèi)存,并記錄在一個(gè)空閑列表(free list)之中。當(dāng)需要新建對(duì)象時(shí),內(nèi)存管理模塊便會(huì)從該空閑列表中尋找空閑內(nèi)存,并劃分給新建的對(duì)象。
清除這種回收方式的原理及其簡(jiǎn)單,但是有兩個(gè)缺點(diǎn):
- 造成
內(nèi)存碎片。由于 Java 虛擬機(jī)的堆中對(duì)象必須是連續(xù)分布的,因此可能出現(xiàn)總空閑內(nèi)存足夠,但是無(wú)法分配的極端情況。比如:總空間100M,此時(shí)我們需要申請(qǐng)100M的數(shù)組。但是由于內(nèi)存不連續(xù),因此我們就會(huì)申請(qǐng)失敗。 - 分配效率較低。如果是一塊連續(xù)的內(nèi)存空間,那么我們可以通過(guò)指針加法(pointer bumping)來(lái)做分配。而對(duì)于空閑列表,Java 虛擬機(jī)則需要逐個(gè)訪問(wèn)列表中的項(xiàng),來(lái)查找能夠放入新建對(duì)象的空閑內(nèi)存。
3.2、壓縮(compact)
常見(jiàn)的一種叫法:標(biāo)記整理
思想:把存活的對(duì)象聚集到內(nèi)存區(qū)域的起始位置,從而留下一段連續(xù)的內(nèi)存空間。
這種做法優(yōu)缺點(diǎn)都比較的明顯:
優(yōu)點(diǎn):能夠解決內(nèi)存碎片化的問(wèn)題缺點(diǎn):壓縮算法的性能開銷
3.3、復(fù)制(copy)
思想:把內(nèi)存區(qū)域分為兩等分,分別用兩個(gè)指針 from 和 to 來(lái)維護(hù),并且只是用 from 指針指向的內(nèi)存區(qū)域來(lái)分配內(nèi)存。當(dāng)發(fā)生垃圾回收時(shí),便把存活的對(duì)象復(fù)制到 to 指針指向的內(nèi)存區(qū)域中,并且交換 from 指針和 to 指針的內(nèi)容。
這種做法的優(yōu)缺點(diǎn)同樣明顯:
優(yōu)點(diǎn):能夠解決內(nèi)存碎片化的問(wèn)題缺點(diǎn):堆空間的使用效率極其低下(畢竟分成兩半,一次只使用一半)
4、現(xiàn)代設(shè)計(jì)方案
經(jīng)研究發(fā)現(xiàn),大多數(shù)的對(duì)象其實(shí)死的很快。因此,現(xiàn)在的垃圾回收器采用上述三種方式并存的思路:
一般是把Java堆分作新生代和老年代,根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ǎ?/p>
新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就用復(fù)制算法,只要少量復(fù)制成本就可以完成收集。老年代中因?yàn)閷?duì)象的存活率較高、周期長(zhǎng),就用標(biāo)記-整理或標(biāo)記-清除算法來(lái)回收。稍稍正式官方的描述:Java 虛擬機(jī)將堆劃分為新生代和老年代。其中,新生代又被劃分為 Eden 區(qū),以及兩個(gè)大小相同的 Survivor 區(qū)。分別用 from 和 to 來(lái)指代。其中 to 指向的 Survivior 區(qū)是空的。
如果Eden區(qū)不夠分配,那么就會(huì)觸發(fā)Minor GC。而此時(shí)Eden 區(qū)和 from 指向的 Survivor 區(qū)中的存活對(duì)象會(huì)被復(fù)制到 to 指向的 Survivor 區(qū)中,然后交換 from 和 to 指針,以保證下一次 Minor GC 時(shí),to 指向的 Survivor 區(qū)還是空的。
Java 虛擬機(jī)會(huì)記錄 Survivor 區(qū)中的對(duì)象一共被來(lái)回復(fù)制了幾次。如果一個(gè)對(duì)象被復(fù)制的次數(shù)為 15,
可以通過(guò)虛擬機(jī)參數(shù) -XX:+MaxTenuringThreshold進(jìn)行設(shè)置。
那么該對(duì)象將被放到老年代。另外,如果單個(gè) Survivor 區(qū)已經(jīng)被占用了 50%,
可以通過(guò)虛擬機(jī)參數(shù) -XX:TargetSurvivorRatio進(jìn)行設(shè)置
那么較高復(fù)制次數(shù)的對(duì)象也會(huì)被晉升至老年代。
而以上的過(guò)程,可以用一個(gè)圖,輕松的描述清楚:

轉(zhuǎn)載地址:https://blog.csdn.net/qq_21383435/article/details/106310383