一文看懂 Java GC 算法背景原理與內(nèi)存池劃分
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來(lái),我們一起精進(jìn)!你不來(lái),我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!
編輯:業(yè)余草
blog.csdn.net/qq_34115899
推薦:https://www.xttblog.com/?p=5336
一文看懂 Java GC 算法背景原理與內(nèi)存池劃分!
文章目錄
引用計(jì)數(shù) 標(biāo)記-清除算法(Mark and Sweep) 標(biāo)記可達(dá)對(duì)象(Marking Reachable Objects) 清除(Sweeping) 標(biāo)記-清除-整理算法(Mark-Sweep-Compact) 分代假設(shè) 內(nèi)存池劃分 年輕代(Young Gen) 老年代 (Old Gen) 永久代 (Perm Gen) 5.4 元數(shù)據(jù)區(qū) (Metaspace)
引用計(jì)數(shù)
通過(guò)在對(duì)象頭中分配一個(gè)空間來(lái)保存該對(duì)象被引用的次數(shù)。如果該對(duì)象被其它對(duì)象引用,則它的引用計(jì)數(shù)加一,如果刪除對(duì)該對(duì)象的引用,那么它的引用計(jì)數(shù)就減一。(一般不是一個(gè)對(duì)象被引用的次數(shù)為0了就立即釋放,出于效率考慮,系統(tǒng)總是會(huì)等一批對(duì)象一起處理,這樣更加高效)
如果A對(duì)象引用B對(duì)象,B對(duì)象引用A對(duì)象,引用計(jì)數(shù)始終不為0,這種循環(huán)依賴的對(duì)象沒(méi)辦法回收。
這種情況在計(jì)算機(jī)中叫做“ 內(nèi)存泄漏 ”,該釋放的沒(méi)釋放,該回收的沒(méi)回收。如果依賴關(guān)系更復(fù)雜,計(jì)算機(jī)的內(nèi)存資源很可能用滿,或者說(shuō)不夠用,內(nèi)存不夠用則稱為“ 內(nèi)存溢出 ”。
標(biāo)記-清除算法(Mark and Sweep)
前面我們講解了引用計(jì)數(shù)里需要查找所有的對(duì)象計(jì)數(shù)和對(duì)象之間的引用關(guān)系。那么如何來(lái)查找所有對(duì)象,怎么來(lái)做標(biāo)記呢?
這個(gè)算法包含兩步:
Marking(標(biāo)記): 遍歷所有的可達(dá)對(duì)象,并在本地內(nèi)存(native)中分門別類記下。Sweeping(清除): 這一步保證不可達(dá)對(duì)象所占用的內(nèi)存被清除,在之后進(jìn)行內(nèi)存分配時(shí)可以重用。
標(biāo)記可達(dá)對(duì)象(Marking Reachable Objects)

首先,有一些特定的對(duì)象被指定為 Garbage Collection Roots(GC根元素)。包括:
局部變量 活動(dòng)線程( Active threads)內(nèi)存中所有類的靜態(tài)字段( static field)JNI引用
如上圖,從GC「根元素」開(kāi)始掃描,到直接引用,以及其他對(duì)象(通過(guò)對(duì)象的屬性域)。所有GC訪問(wèn)到的對(duì)象都被「標(biāo)記」(marked)為存活對(duì)象。
存活對(duì)象在上圖中用藍(lán)色表示。標(biāo)記階段完成后,所有存活對(duì)象都被標(biāo)記了。而其他對(duì)象就是從GC根元素不可達(dá)的,也就是說(shuō)程序不能再使用這些不可達(dá)的對(duì)象(unreachable object)。這樣的對(duì)象被認(rèn)為是垃圾,GC會(huì)在接下來(lái)的階段中清除他們。
「標(biāo)記清除算法最重要的優(yōu)點(diǎn),就是解決了引用計(jì)數(shù)的循環(huán)引用而導(dǎo)致內(nèi)存泄露的問(wèn)題,因?yàn)橹粯?biāo)記可達(dá)對(duì)象,如果一系列對(duì)象形成了環(huán),但這個(gè)環(huán)上的所有對(duì)象都是不可達(dá)的,那么在引用跟蹤的可達(dá)對(duì)象集合里就不包括環(huán),環(huán)上的對(duì)象都屬于不可達(dá)對(duì)象,都會(huì)被清除。」
JVM中包含了多種GC算法,如Parallel Scavenge(并行清除),Parallel Mark+Copy(并行標(biāo)記+復(fù)制) 以及 CMS,他們?cè)趯?shí)現(xiàn)上略有不同,但理論上都采用了以上兩個(gè)步驟。
這種方法也有不好的地方:在標(biāo)記階段,需要暫停所有應(yīng)用線程,去遍歷所有對(duì)象的引用關(guān)系,因?yàn)椴粫和>蜎](méi)法跟蹤一直在變化的引用關(guān)系圖。這種情景叫做 「Stop The World pause」 (「STW全線停頓」),也叫做GC停頓。有一個(gè)概念需要注意,可以安全地暫停線程的點(diǎn)叫做「安全點(diǎn)」(safe point)。GC過(guò)程中,有一部分操作需要等所有應(yīng)用線程都到達(dá)安全點(diǎn),然后暫停所有線程進(jìn)行GC,這樣JVM就可以專心執(zhí)行清理工作。安全點(diǎn)可能有多種因素觸發(fā),但GC是最主要的原因。
標(biāo)記階段暫停的時(shí)間與堆內(nèi)存大小、對(duì)象的總數(shù)沒(méi)有直接關(guān)系,而是由「存活對(duì)象」(alive objects)的數(shù)量來(lái)決定。所以增加堆內(nèi)存的大小并不會(huì)直接影響標(biāo)記階段占用的時(shí)間。
清除(Sweeping)
Mark and Sweep(標(biāo)記-清除) 算法的概念非常簡(jiǎn)單,在標(biāo)記階段完成后,所有不可達(dá)對(duì)象占用的內(nèi)存空間會(huì)被清除,可以用來(lái)分配新對(duì)象。
這種算法需要使用空閑表(free-list)來(lái)記錄所有的空閑區(qū)域,以及每個(gè)區(qū)域的大小。維護(hù)空閑表增加了對(duì)象分配時(shí)的開(kāi)銷。此外還存在另一個(gè)弱點(diǎn) —— 明明還有很多空閑內(nèi)存,但是卻都不連續(xù),導(dǎo)致寫入操作越來(lái)越耗時(shí),因?yàn)閷ふ乙粔K足夠大的空閑內(nèi)存變得困難,可能最后沒(méi)有一個(gè)區(qū)域的大小能夠存放需要分配的對(duì)象,從而導(dǎo)致內(nèi)存分配失敗(在Java 中就是 OutOfMemoryError),這個(gè)就叫「內(nèi)存的碎片化」。就像是擺滿棋子的圍棋盤上,一部分位置上棋子被拿掉而產(chǎn)生了一些零散的空位置,沒(méi)有一整片的空地方。

標(biāo)記-清除-整理算法(Mark-Sweep-Compact)
這個(gè)算法就是在標(biāo)記-清除算法的基礎(chǔ)上多了整理(Compact)這一步。也就是將所有被標(biāo)記的對(duì)象(存活對(duì)象),遷移到內(nèi)存空間的起始處,「消除了標(biāo)記-清除算法的缺點(diǎn)」。
但是也有缺點(diǎn),是GC暫停時(shí)間會(huì)增加,因?yàn)樾枰獙⑺袑?duì)象復(fù)制到另一個(gè)地方,然后修改指向這些對(duì)象的引用,這也是需要額外時(shí)間的。
?
JVM中的引用是一個(gè)抽象的概念,如果GC移動(dòng)某個(gè)對(duì)象,就會(huì)修改(棧和堆中)所有指向該對(duì)象的引用。移動(dòng)/拷貝/提升/壓縮 一般來(lái)說(shuō)是一個(gè)
?STW的過(guò)程,所以修改對(duì)象引用是一個(gè)安全的行為。但因?yàn)橐滤械囊?,可能?huì)影響應(yīng)用程序的性能。
碎片整理之后,分配新對(duì)象就很簡(jiǎn)單,只需要通過(guò)「指針碰撞」(pointer bumping)即可。使用這種算法,內(nèi)存空間剩余的容量一直是清楚的,不會(huì)再導(dǎo)致內(nèi)存碎片問(wèn)題。
?指針碰撞:假設(shè)
?Java堆中內(nèi)存時(shí)完整的,已分配的內(nèi)存和空閑內(nèi)存分別在不同的一側(cè),通過(guò)一個(gè)指針作為分界點(diǎn),需要分配內(nèi)存時(shí),僅僅需要把指針往空閑的一端移動(dòng)與對(duì)象大小相等的距離。

在這里思考一個(gè)問(wèn)題,任何一種GC算法能否高效的管理當(dāng)前堆內(nèi)存的所有對(duì)象?答案是否定的,因?yàn)閷?duì)象的生命周期都不同,有的很快被清除,有的還會(huì)存活很長(zhǎng)時(shí)間。于是就有了「分代假設(shè)理論」…
分代假設(shè)
我們前面提到過(guò),執(zhí)行垃圾收集需要停止整個(gè)應(yīng)用。很明顯,對(duì)象越多則收集所有垃圾消耗的時(shí)間就越長(zhǎng)。但可不可以只處理一個(gè)較小的內(nèi)存區(qū)域呢? 為了探究這種可能性,做了一個(gè)分析圖。

于是便有了分代假設(shè):
大部分新生對(duì)象很快無(wú)用; 存活較長(zhǎng)時(shí)間的對(duì)象,可能存活更長(zhǎng)時(shí)間。
我們可以根據(jù)對(duì)象的不同特點(diǎn),把對(duì)象進(jìn)行分類。基于這一假設(shè),VM中的內(nèi)存被分為「年輕代」(Young Generation)和「老年代」(Old Generation)。老年代有時(shí)候也稱為年老區(qū)(Tenured)。
拆分為這樣兩個(gè)可清理的單獨(dú)區(qū)域,我們就可以「根據(jù)對(duì)象的不同特點(diǎn),允許采用不同的算法來(lái)大幅提高GC的性能。」
要著重強(qiáng)調(diào)的是,分代假設(shè)并不適用于所有程序。因?yàn)榉执?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">GC算法專門針對(duì)“要么死得快”,“否則活得長(zhǎng)” 這類特征的對(duì)象來(lái)進(jìn)行優(yōu)化,此時(shí)JVM管理那種存活時(shí)間半長(zhǎng)不長(zhǎng)的對(duì)象就顯得非常尷尬了。
內(nèi)存池劃分

我們把新對(duì)象的創(chuàng)建放在年輕代,把長(zhǎng)期存活的對(duì)象放在老年代。這樣就可以用不同的策略去優(yōu)化這兩塊對(duì)象內(nèi)存的管理方式。
年輕代(Young Gen)
「年輕代」可以分為**新生代 (Eden Space)和兩個(gè)存活區(qū)(Survivor Spaces)——S0和S1**,新生代也叫Eden區(qū),是分配創(chuàng)建新對(duì)象的地方。
當(dāng)Eden區(qū)要滿的時(shí)候,就會(huì)做一次垃圾回收,在垃圾回收的階段會(huì)先標(biāo)記存活對(duì)象,然后會(huì)把Eden區(qū)里存活的對(duì)象復(fù)制到其中一個(gè)存活區(qū),假設(shè)是S0。這個(gè)時(shí)候隨著程序的運(yùn)行,對(duì)象繼續(xù)創(chuàng)建,因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">S0在上個(gè)階段已經(jīng)往里面復(fù)制了一部分對(duì)象,這時(shí)Eden區(qū)再次要滿的時(shí)候,就會(huì)把Eden區(qū)和S0里存活的對(duì)象復(fù)制到另一個(gè)存活區(qū)S1,復(fù)制完后,Eden區(qū)和S0區(qū)所有的對(duì)象占用的內(nèi)存都是可以被釋放的,所以直接把Eden區(qū)和S0給清空掉。
然后在下一個(gè)使用周期繼續(xù)在Eden區(qū)創(chuàng)建對(duì)象,Eden區(qū)再滿的時(shí)候,就會(huì)把Eden區(qū)和S1區(qū)里的存活對(duì)象復(fù)制到S0,再把Eden和S1清空掉。需要注意的時(shí),通常Eden區(qū)、S0區(qū)、S1區(qū)中都只有兩個(gè)區(qū)域有數(shù)據(jù),另外一個(gè)存活區(qū)是空的。年輕代絕大部分對(duì)象會(huì)在垃圾回收的時(shí)候被清理掉,只有少量對(duì)象會(huì)存活,所以每次垃圾回收只要把這少量對(duì)象標(biāo)記出來(lái),然后復(fù)制到其中一個(gè)存活區(qū)即可。
?每個(gè)周期里都會(huì)把少量對(duì)象從一個(gè)存活區(qū)復(fù)制到另一個(gè)存活區(qū),所以這兩個(gè)存活區(qū)除了叫
?S0和S1以外,也可以叫from和to,復(fù)制的起點(diǎn)存活區(qū)叫from,目標(biāo)存活區(qū)叫to
注意:這里用到的算法是「標(biāo)記-復(fù)制」算法,不需要做移動(dòng)。因?yàn)閺?fù)制了一份,原有的數(shù)據(jù)可以清空掉。如下圖:

細(xì)心地小伙伴肯定發(fā)現(xiàn)了,圖中怎么還會(huì)有一個(gè)TLAB的區(qū)域?
因?yàn)闀?huì)有多個(gè)線程同時(shí)創(chuàng)建多個(gè)對(duì)象,所以 Eden 區(qū)被劃分為多個(gè)線程本地分配緩沖區(qū)(Thread Local Allocation Buffer,簡(jiǎn)稱TLAB)。
通過(guò)這種緩沖區(qū)劃分,大部分對(duì)象直接由JVM 在對(duì)應(yīng)線程的TLAB中分配,避免與其他線程的同步操作。
如果 TLAB 中沒(méi)有足夠的內(nèi)存空間,就會(huì)在共享Eden區(qū)(shared Eden space)中分配。如果共享Eden區(qū)也沒(méi)有足夠的空間,就會(huì)觸發(fā)一次年輕代GC 來(lái)釋放內(nèi)存空間。如果GC之后 Eden 區(qū)依然沒(méi)有足夠的空閑內(nèi)存區(qū)域,則對(duì)象就會(huì)被分配到老年代空間(Old Generation)。
老年代 (Old Gen)
從上圖可以看到,在GC后,對(duì)象有一部分去了老年代,這個(gè)細(xì)節(jié)很重要。
如果對(duì)象經(jīng)歷了一定的GC次數(shù)后仍然存活,那么它們就會(huì)挪到老年代。比如默認(rèn)情況下是15次(這也是HotSpot JVM中允許的最大值),通過(guò)-XX: +MaxTenuringThreshold=15設(shè)置最大老年代的閾值。因?yàn)閷?duì)象存活周期超過(guò)15次,有很大概率這個(gè)對(duì)象會(huì)繼續(xù)存活很多代,所以放在老年代。
「如果存活區(qū)空間不夠存放年輕代中的存活對(duì)象,對(duì)象提升(Promotion)到老年代的步驟也可能更早地進(jìn)行。」
老年代內(nèi)存空間通常會(huì)更大,里面的對(duì)象是垃圾的概率也更小。老年代GC發(fā)生的頻率比年輕代小很多。同時(shí),因?yàn)轭A(yù)期老年代中的對(duì)象大部分是存活的,所以不再使用標(biāo)記和復(fù)制(Mark and Copy)算法。而是采用移動(dòng)對(duì)象的方式來(lái)實(shí)現(xiàn)最小化內(nèi)存碎片。老年代空間的清理算法通常是建立在不同的基礎(chǔ)上的。原則上,會(huì)執(zhí)行以下這些步驟
先標(biāo)記所有的根可達(dá)對(duì)象 刪除不可達(dá)對(duì)象 整理老年代空間里存活的對(duì)象,方法是將所有的存活對(duì)象復(fù)制,從老年代空間開(kāi)始的地方依次存放
整理的方法就是把所有存活對(duì)象進(jìn)行復(fù)制,移動(dòng)到老年代空間開(kāi)始的地方,依次往后對(duì)齊存放,也就是做了內(nèi)存的碎片整理。
「為什么用這種復(fù)制移動(dòng)方式處理?」 因?yàn)槔夏甏鷽](méi)有繼續(xù)分區(qū),沒(méi)辦法像年輕代一樣有兩個(gè)存活區(qū)交替進(jìn)行復(fù)制清除,只能進(jìn)行整理,避免碎片過(guò)多。
永久代 (Perm Gen)
在Java 8 之前有一個(gè)特殊的空間,稱為“永久代”(Permanent Generation)。這是存儲(chǔ)元數(shù)據(jù)(metadata)的地 方,比如 class 信息等。此外,「這個(gè)區(qū)域中也保存有其他的數(shù)據(jù)和信息,包括內(nèi)部化的字符串(internalized strings)等等。實(shí)際上這給Java開(kāi)發(fā)者造成了很多麻煩,因?yàn)楹茈y去計(jì)算這塊區(qū)域到底需要占用多少內(nèi)存空間?!?/strong> 預(yù)測(cè)失敗導(dǎo)致的結(jié)果就是產(chǎn)生 java.lang.OutOfMemoryError: Permgen space 這種形式的錯(cuò)誤。除非OutOfMemoryError 確實(shí)是內(nèi)存泄漏導(dǎo)致的,否則就只能增加 permgen 的大小,例如下面的示例,就 是設(shè)置 perm gen 最大空間為 256 MB:
-XX:MaxPermSize=256m
元數(shù)據(jù)區(qū) (Metaspace)
既然估算元數(shù)據(jù)所需空間那么復(fù)雜,Java 8直接刪除了永久代(Permanent Generation),改用 Metaspace。從此以后,Java 中很多雜七雜八的東西都放置到普通的堆內(nèi)存里。當(dāng)然,像類定義(class definitions)之類的信息會(huì)被加載到 Metaspace 中。元數(shù)據(jù)區(qū)位于本地內(nèi)存(native memory),不再影響到普通的Java對(duì)象。默認(rèn)情況下,Metaspace的大小只受限于 Java 進(jìn)程可用的本地內(nèi)存。這樣程序就不再因?yàn)槎嗉虞d了幾個(gè)類/JAR包就導(dǎo)致 java.lang.OutOfMemoryError: Permgen space. 。注意,這種不受限制的空間也不是沒(méi)有代價(jià)的 —— 如果 Metaspace 失控,則可能會(huì)導(dǎo)致嚴(yán)重影 響程序性能的內(nèi)存交換(swapping),或者導(dǎo)致本地內(nèi)存分配失敗。如果需要避免這種最壞情況,那么可以通過(guò)下面這樣的方式來(lái)限制 Metaspace 的大小,如 256 MB
-XX:MaxMetaspaceSize=256m
歡迎一鍵三連~
有問(wèn)題請(qǐng)留言,大家一起探討學(xué)習(xí)
