新聞中心
來(lái)說(shuō)說(shuō)垃圾回收怎么樣~
作者: 鴨血粉絲 2020-11-27 07:45:31
云計(jì)算
虛擬化 JVM 的自動(dòng)內(nèi)存管理,讓原本應(yīng)該是開(kāi)發(fā)人員去做的事情,變成了垃圾回收器來(lái)做的事情,既然是別人幫忙做的事情,那么可能就不是自己想要的,所以就需要我們了解一下垃圾回收相關(guān)的內(nèi)容。

成都創(chuàng)新互聯(lián)成都網(wǎng)站建設(shè)按需定制,是成都網(wǎng)站建設(shè)公司,為陽(yáng)光房提供網(wǎng)站建設(shè)服務(wù),有成熟的網(wǎng)站定制合作流程,提供網(wǎng)站定制設(shè)計(jì)服務(wù):原型圖制作、網(wǎng)站創(chuàng)意設(shè)計(jì)、前端HTML5制作、后臺(tái)程序開(kāi)發(fā)等。成都網(wǎng)站改版熱線:13518219792
本文轉(zhuǎn)載自微信公眾號(hào)「Java極客技術(shù)」,作者鴨血粉絲 。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java極客技術(shù)公眾號(hào)。
JVM 的自動(dòng)內(nèi)存管理,讓原本應(yīng)該是開(kāi)發(fā)人員去做的事情,變成了垃圾回收器來(lái)做的事情
既然是別人幫忙做的事情,那么可能就不是自己想要的,所以就需要我們了解一下垃圾回收相關(guān)的內(nèi)容
引用計(jì)數(shù)法與可達(dá)性分析
垃圾回收,垃圾回收,那就是有的內(nèi)存分配給了一些對(duì)象,但是這些對(duì)象已經(jīng)用完了,那么它所占用的內(nèi)存也就應(yīng)該該釋放掉了,卻還沒(méi)有釋放
那么,這里就有個(gè)問(wèn)題:該如何確定一個(gè)對(duì)象用完了呢?
其中一種方法就是引用計(jì)數(shù)法
引用計(jì)數(shù)法就是給每個(gè)對(duì)象添加一個(gè)引用計(jì)數(shù)器,來(lái)統(tǒng)計(jì)指向該對(duì)象的引用個(gè)數(shù)
比如:如果有一個(gè)引用,被賦值為某一個(gè)對(duì)象,那么這個(gè)對(duì)象的引用計(jì)數(shù)器就 +1 ,如果一個(gè)指向這個(gè)對(duì)象的引用,被賦值為了其他的值,那么這個(gè)對(duì)象的引用計(jì)數(shù)器就 -1 ,這樣如果這個(gè)對(duì)象的引用計(jì)數(shù)器為 0 ,我們就可以認(rèn)為這個(gè)對(duì)象已經(jīng)使用完畢,它所占用的內(nèi)存空間可以回收掉了
這種方案聽(tīng)上去無(wú)懈可擊,但是有一個(gè)致命的漏洞,就是沒(méi)辦法處理循環(huán)引用的問(wèn)題
比如說(shuō): A 和 B 互相引用,除此之外也沒(méi)有其他的引用指向 A 或者 B ,在這種情況下,其實(shí) A 和 B 所占用的內(nèi)存就可以釋放掉了,但是因?yàn)樗鼈兓ハ喽加幸?,所以此時(shí)的引用計(jì)數(shù)器并不為 0 ,在這種情況下,就不能對(duì)它們進(jìn)行回收
現(xiàn)在只是兩個(gè)對(duì)象,如果再來(lái)兩個(gè),再來(lái)兩個(gè),這樣循環(huán)引用的對(duì)象多了之后,就會(huì)造成內(nèi)存泄露
基于引用計(jì)數(shù)法的弊端,當(dāng)前 JVM 主流的垃圾回收器采取的是可達(dá)性分析算法
這個(gè)算法本質(zhì)就是將一系列的 GC Roots 作為初始的存活對(duì)象合集( live set ),然后從這個(gè)合集出發(fā),探索所有能夠被該集合引用到的對(duì)象,并把這些對(duì)象加入到集合中來(lái),這個(gè)過(guò)程就叫做標(biāo)記( mark ),遍歷到最后,沒(méi)有被探索到的對(duì)象就是可以回收的對(duì)象
那么什么是 GC Roots 嘞?一般包括(但不限于)以下幾種:
- Java 方法棧楨中的局部變量
- 已加載類的靜態(tài)變量
- JNI handles
- 已啟動(dòng)并且沒(méi)有停止的 Java 線程
剛才說(shuō)因?yàn)橐糜?jì)數(shù)法存在循環(huán)引用的問(wèn)題,所以目前主流垃圾回收器選用的都是可達(dá)性分析法,也就是說(shuō),它解決了循環(huán)引用問(wèn)題,其實(shí)這一點(diǎn)也比較好理解,雖然 A 和 B 相互引用,但是這個(gè)時(shí)候從 GC Roots 開(kāi)始出發(fā),是沒(méi)有辦法到達(dá) A 和 B 的,那么就不會(huì)把它們放到存活對(duì)象合集之中,自然也就會(huì)被回收掉
但是在實(shí)際中還是會(huì)有問(wèn)題的,比如:在多線程環(huán)境下,就會(huì)有其他線程更新已經(jīng)訪問(wèn)過(guò)的對(duì)象中的引用,但是是多線程并行的嘛,這個(gè)時(shí)候可達(dá)性分析法已經(jīng)把這個(gè)引用設(shè)置成了 null ,或者這個(gè)對(duì)象還在使用,但可達(dá)性分析法把它標(biāo)記為了沒(méi)有被訪問(wèn)過(guò)的對(duì)象,被回收掉了,這種情況可能直接導(dǎo)致 JVM 崩潰掉
Stop-the-world & safepoint
既然可達(dá)性分析法也有自己的一些缺陷,總得有解決方案吧?比較暴力的一種方法就是 Stop-the-world ,估計(jì)聽(tīng)名字也能知道,就是讓全世界都停下來(lái),也就是說(shuō),在進(jìn)行垃圾回收的時(shí)候,其他所有非垃圾回收線程的工作都需要停下來(lái),先讓垃圾回收器工作完畢再說(shuō)。這就是所謂的暫停時(shí)間( GC pause )
Stop-the-world 是通過(guò)安全點(diǎn)( safepoint )機(jī)制來(lái)實(shí)現(xiàn)的。啥意思嘞?咱先想個(gè)場(chǎng)景,現(xiàn)在你敲代碼敲的特別開(kāi)心,又有思路,狀態(tài)又好,美滋滋的正在工作,突然毫無(wú)緣由的就讓你現(xiàn)在不準(zhǔn)敲代碼,你會(huì)不會(huì)不開(kāi)心?好不容易思路來(lái)了對(duì)吧,就一點(diǎn)兒理由都不給的就讓我停下,不合理吧?
同樣的場(chǎng)景,一個(gè)線程現(xiàn)在跑的特別 happy ,而且再有一秒鐘就完成了任務(wù),這個(gè)時(shí)候 JVM 收到了 Stop-the-world 請(qǐng)求,二話不說(shuō)就把所有的線程給停掉,不太好吧?那么這個(gè)時(shí)候安全點(diǎn)( safepoint )機(jī)制就登場(chǎng)了。有了安全點(diǎn)機(jī)制,當(dāng) JVM 收到 Stop-the-world 請(qǐng)求的時(shí)候,它就會(huì)等待所有的線程都達(dá)到安全點(diǎn),才允許請(qǐng)求 Stop-the-world 的線程進(jìn)行獨(dú)占的工作
那么,什么時(shí)候是安全點(diǎn)呢?舉個(gè)例子來(lái)說(shuō):當(dāng) Java 程序通過(guò) JNI 執(zhí)行本地代碼時(shí),如果這段代碼不訪問(wèn) Java 對(duì)象,不調(diào)用 Java 方法,不返回到原 Java 方法,那么 Java 虛擬機(jī)的堆棧就不會(huì)發(fā)生改變,那這段本地代碼就可以作為一個(gè)安全點(diǎn)。只要不離開(kāi)這個(gè)安全點(diǎn), JVM 就可以在垃圾回收的同時(shí),繼續(xù)運(yùn)行這段本地代碼
因?yàn)楸镜卮a需要通過(guò) JNI 的 API 來(lái)完成上述三個(gè)操作,因此 JVM 只需要在 API 的入口處進(jìn)行安全點(diǎn)檢測(cè)( safepoint poll ),看看有沒(méi)有其他線程請(qǐng)求停留在安全點(diǎn)這里,就可以在必要的時(shí)候掛起當(dāng)前線程
垃圾回收的三種方式
當(dāng)標(biāo)記好存活的對(duì)象之后,就可以進(jìn)行垃圾回收了
主流的垃圾回收方式,可以分為三種:清除( sweep ),壓縮( compact ),復(fù)制( copy )
清除,就是把死亡對(duì)象所占據(jù)的內(nèi)存標(biāo)記成空閑內(nèi)存,并把它記錄在一個(gè)空閑列表( free list )中,當(dāng)需要新建對(duì)象的時(shí)候,就直接在空閑列表中尋找空閑內(nèi)存,劃分給新建的對(duì)象就完了
但是這里會(huì)產(chǎn)生一個(gè)問(wèn)題,因?yàn)樗劳龅膶?duì)象所占據(jù)的內(nèi)存可能是隨機(jī)的,回收完畢之后,內(nèi)存就是碎片化的,如果此時(shí)有對(duì)象申請(qǐng)一塊連續(xù)的內(nèi)存空間,盡管碎片化的內(nèi)存空間是夠用的,也沒(méi)辦法進(jìn)行分配
壓縮,就是把存活的對(duì)象聚集到內(nèi)存區(qū)域的起始位置,這樣就可以留下一段連續(xù)的內(nèi)存空間。這樣去做的話,可以解決內(nèi)存碎片化的問(wèn)題,代價(jià)就是壓縮算法帶來(lái)的性能開(kāi)銷
復(fù)制,就是把內(nèi)存區(qū)域分成兩等分,分別用兩個(gè)指針 from 和 to 來(lái)維護(hù),并且只是用 from 指針指向的內(nèi)存區(qū)域來(lái)分配內(nèi)存。當(dāng)進(jìn)行垃圾回收時(shí),就把存活的對(duì)象復(fù)制到 to 指針指向的內(nèi)存區(qū)域中,并且交換 from 指針和 to 指針的內(nèi)容。
復(fù)制這種方式也可以解決內(nèi)存碎片化的問(wèn)題,但是它的缺點(diǎn)也是比較明顯的,因?yàn)榘褍?nèi)存區(qū)域分成了兩等分嘛,那利用率就比較低咯,最高也是 50% 了,不能再高了
垃圾回收在 JVM 中的應(yīng)用
上面說(shuō)的三種垃圾回收方式是理論上的,那么在 JVM 中是如何應(yīng)用的呢?
這就先要來(lái)了解下 JVM 的堆劃分,大概就是這樣子:
JVM 將堆劃分為新生代和老年代,在新生代中又劃分為 Eden 區(qū),還有兩個(gè)大小相同的 Survivor 區(qū)
當(dāng)程序調(diào)用 new 指令時(shí),會(huì)在 Eden 區(qū)中劃出一塊作為存儲(chǔ)對(duì)象的內(nèi)存,但是因?yàn)槎芽臻g是線程共享的,所以在這里面劃分空間的話就需要同步,要不然出現(xiàn)了兩個(gè)對(duì)象共用一段內(nèi)存,那不就該打架了嘛
JVM 為了避免兩個(gè)對(duì)象打架的事情發(fā)生,就讓每個(gè)線程向 JVM 申請(qǐng)一段連續(xù)的內(nèi)存,來(lái)作為線程私有的 TLAB ( Thread Local Allocation Buffer ,對(duì)應(yīng)虛擬機(jī)參數(shù) -XX:+UseTLAB ,默認(rèn)開(kāi)啟的)
Eden 區(qū)一直進(jìn)行分配,總有空間分配完畢的時(shí)候,該怎么辦?此時(shí) JVM 就會(huì)觸發(fā)一次 Minor GC ,來(lái)收集新生代的垃圾,存活下來(lái)的對(duì)象就會(huì)被送到 Survivor 區(qū)
在圖中可以看到, Survivor 區(qū)有兩個(gè),一個(gè)是 from ,一個(gè)是 to ,其中 to 指向的 Survivor 區(qū)是空的
當(dāng)發(fā)生 Minor GC 時(shí), Eden 區(qū)和 from 指向的 Survivor 區(qū)中的存活對(duì)象會(huì)被復(fù)制到 to 指向的 Survivor 區(qū),然后交換 from 和 to 指針,這樣就保證了下一次 Minor GC 時(shí), to 指向的 Survivor 區(qū)還是空的
同時(shí) JVM 會(huì)記錄 Survivor 區(qū)的對(duì)象一共被來(lái)回復(fù)制了幾次,如果一個(gè)對(duì)象被復(fù)制的次數(shù)為 15 (對(duì)應(yīng)虛擬機(jī)參數(shù) -XX:+MaxTenuringThreshold ),這個(gè)對(duì)象就會(huì)被晉升( promote )到老年代
那么在發(fā)生 Minor GC 時(shí),采用哪種垃圾回收方式會(huì)比較好一些呢?采用復(fù)制方式,也就是 標(biāo)記-復(fù)制 算法會(huì)好一些。為什么呢?因?yàn)樵谛律校蟛糠值?Java 對(duì)象只存活一小段時(shí)間,那么我們就可以采用耗時(shí)比較短的垃圾回收算法,讓大部分的垃圾都能在新生代被回收掉。使用 標(biāo)記-復(fù)制 算法的話,理想情況下就是 Eden 區(qū)中的對(duì)象基本都死亡了,那么需要復(fù)制的數(shù)據(jù)非常少,此時(shí)這種算法的優(yōu)勢(shì)就被極大的體現(xiàn)了出來(lái)
網(wǎng)站題目:來(lái)說(shuō)說(shuō)垃圾回收怎么樣~
標(biāo)題路徑:http://www.dlmjj.cn/article/ccidjic.html


咨詢
建站咨詢
