【深入了解】Java核心機(jī)制之垃圾回收

一、簡介

我們知道,程序在運(yùn)行的時候,為了提高性能,大部分?jǐn)?shù)據(jù)都是會加載到內(nèi)存中進(jìn)行運(yùn)算的,有些數(shù)據(jù)是需要常駐內(nèi)存中的,但是有些數(shù)據(jù),用過之后便不會再需要了,我們稱這部分?jǐn)?shù)據(jù)為垃圾數(shù)據(jù)。接下來博主就來講一講垃圾回收機(jī)制的原理,有什么不懂的可以私信博主。

  • 為了防止內(nèi)存被使用完,我們需要將這些垃圾數(shù)據(jù)進(jìn)行回收,即需要將這部分內(nèi)存空間進(jìn)行釋放。不同于 C++ 需要自行釋放內(nèi)存的機(jī)制,Java 虛擬機(jī)(JVM)提供了一種自動回收內(nèi)存的機(jī)制,這對于我們開發(fā)人員來說,再友好不過了。

  • 不同于 C++ 程序,C++ 是需要開發(fā)人員自己分配內(nèi)存并且進(jìn)行自行回收內(nèi)存的,而 Java 程序,內(nèi)存是托管于 JVM 的,即對象的創(chuàng)建和內(nèi)存的回收都是由 JVM 自行完成的,開發(fā)人員是無權(quán)干涉的,只能盡量去優(yōu)化。
    如果有什么問題,下文也會依照這幾點(diǎn)疑問來進(jìn)行深入探討:

JVM 內(nèi)存模型

JVM 內(nèi)存大致分為 線程私有區(qū)域 和 線程共享區(qū)域,并且其主要由5個區(qū)域組成,見下圖:
image.png
  • 由上圖可以看出,虛擬機(jī)棧、本地方法棧和程序計數(shù)器,這三個區(qū)域是線程私有的。比如棧幀的生命周期是和線程關(guān)聯(lián)的,即隨線程而生,隨線程而死。

  • 虛擬機(jī)棧其實(shí)就是用來描述 Java 方法執(zhí)行的,所以每個方法執(zhí)行的時候都會創(chuàng)建一個棧幀,每個棧幀都包含:局部變量、操作數(shù)棧、動態(tài)鏈接、方法出口,當(dāng)方法執(zhí)行完成之后,對應(yīng)的棧幀便會出棧。所以它的內(nèi)存分配是具備確定性的,因此我們并不需要太過關(guān)注包括虛擬機(jī)棧在內(nèi)的這幾個線程私有區(qū)域的內(nèi)存使用情況。

  • 相反,另外兩個線程共享的區(qū)域:方法區(qū)和堆內(nèi)存,則是我們需要重點(diǎn)關(guān)注的對象。因?yàn)檫@兩個區(qū)域主要存放對象、數(shù)組等不具有確定性的數(shù)據(jù),例如創(chuàng)建對象,每個方法運(yùn)行的過程中創(chuàng)建的對象的數(shù)量是不確定的,即占用的內(nèi)存是不確定的,可能不需要創(chuàng)建對象,也可能會創(chuàng)建很多對象,所以我們需要一套合理的內(nèi)存管理機(jī)制來對這兩個區(qū)域進(jìn)行維護(hù),因此,垃圾回收就應(yīng)運(yùn)而生了,并且這兩個區(qū)域也是垃圾回收器進(jìn)行垃圾回收的最重要的內(nèi)存區(qū)域。

  • 我們再來對堆內(nèi)存和方法區(qū)進(jìn)行一下劃分,因?yàn)?JVM 是采用分代回收的算法,即根據(jù)對象的生命周期進(jìn)行區(qū)分并進(jìn)行分代存儲和回收,其主要分為年輕代、老年代、持久代,見下圖:


    image.png
  • 堆內(nèi)存主要由年輕代和老年代組成,而方法區(qū)主要存儲持久代的數(shù)據(jù),詳細(xì)的細(xì)節(jié)在下文講回收算法的時候會細(xì)說。

  • 注意:從 JDK 1.8 開始,永久代已經(jīng)被移除了,取而代之的是元空間(Meta Space),它和服務(wù)器的內(nèi)存相關(guān)聯(lián),本文暫不贅述。

內(nèi)存中的垃圾

  • 程序在運(yùn)行過程中會創(chuàng)建對象,但是當(dāng)方法執(zhí)行完成或當(dāng)這個對象使用完畢之后,它便被定義為了“垃圾”,這時候便需要依靠垃圾回收器去將這塊內(nèi)存區(qū)域清理出來,而對于上述“垃圾”的定義,我們需要將它量化成計算機(jī)語言,即需要設(shè)計一套算法來給垃圾回收器使用,因?yàn)楫吘惯M(jìn)行垃圾回收的動作是垃圾回收器自動運(yùn)行并判定的。

  • 判定一個對象是否是“垃圾”,即判定一個對象的存活與否,常見的算法有兩種:引用計數(shù)法 和 根搜索算法。

引用計數(shù)算法(Reference Counting Collector)

  • 一個對象被創(chuàng)建之后,系統(tǒng)會給這個對象初始化一個引用計數(shù)器,當(dāng)這個對象被引用了,則計數(shù)器 +1,而當(dāng)該引用失效后,計數(shù)器便 -1,直到計數(shù)器為 0,意味著該對象不再被使用了,則可以將其進(jìn)行回收了。

  • 這種算法其實(shí)很好用,判定比較簡單,效率也很高,但是卻有一個很致命的缺點(diǎn),就是它無法避免循環(huán)引用,即兩個對象之間循環(huán)引用的時候,各自的計數(shù)器始終不會變成 0,所以 引用計數(shù)算法 只出現(xiàn)在了早期的 JVM 中,現(xiàn)在基本不再使用了。

根搜索算法(Tracing Collector)

  • 根搜索算法的中心思想,就是從某一些指定的根對象(GC Roots)出發(fā),一步步遍歷找到和這個根對象具有引用關(guān)系的對象,然后再從這些對象開始繼續(xù)尋找,從而形成一個個的引用鏈(其實(shí)就和圖論的思想一致),然后不在這些引用鏈上面的對象便被標(biāo)識為引用不可達(dá)對象,也就是我們說的“垃圾”,這些對象便需要回收掉。這種算法很好地解決了上面 引用計數(shù)算法 的循環(huán)引用的問題了。


    image.png

算法的核心思想是很簡單的,就是標(biāo)記不可達(dá)對象,然后交由 GC 進(jìn)行回收,但是有一個點(diǎn)是很重要的,那就是 何為根對象(GC Roots)?

根對象,一般有如下幾種:

  • 虛擬機(jī)棧中引用的對象(棧幀中的本地變量表);
  • 方法區(qū)中常量引用的對象;
  • 方法區(qū)中靜態(tài)屬性引用的對象;
  • 本地方法棧中 JNI(Native 方法)引用的對象;
  • 活躍線程。

但其實(shí),上述算法只是一個算法的中心思想,實(shí)際執(zhí)行過程是比這個復(fù)雜的,另外,GC 判斷對象是否可達(dá)其實(shí)看的還是強(qiáng)引用。

  • 1、進(jìn)行根搜索的時候,是需要暫停所有線程的,即執(zhí)行一次 STW(Stop The World),最主要的目的是防止上述的對象圖在算法運(yùn)行的過程中有變化從而影響算法的準(zhǔn)確性。
  • 2、線程暫停的時間長短,取決于對象的多少,和堆內(nèi)存的大小無關(guān)。
  • 3、 宣告一個對象的“死亡”其實(shí)不僅僅通過上述的算法計算,而是需要經(jīng)歷兩次的標(biāo)記,本文暫不進(jìn)行贅述。

回收算法

  • 除了需要上文研究的標(biāo)記“垃圾對象”的算法,我們也需要“清理垃圾”的 回收算法。

常用的回收算法一般有:
標(biāo)記-清除算法、標(biāo)記-整理算法、復(fù)制算法,以及系統(tǒng)自動進(jìn)行判定使用的 適應(yīng)性算法。

  • 標(biāo)記 - 清除算法(Tracing Collector)
    標(biāo)記-清除 算法是最基礎(chǔ)的收集算法,它是由 標(biāo)記 和 清除 兩個步驟組成的。

  • 標(biāo)記的過程其實(shí)就是上面的 根搜索算法 所標(biāo)記的不可達(dá)對象,當(dāng)所有的待回收的“垃圾對象”標(biāo)記完成之后,便進(jìn)行第二個步驟:統(tǒng)一清除。

  • 該算法的優(yōu)點(diǎn)是當(dāng)存活對象比較多的時候,性能比較高,因?yàn)樵撍惴ㄖ恍枰幚泶厥盏膶ο螅恍枰幚泶婊畹膶ο蟆?/p>

  • 但是缺點(diǎn)也很明顯,就是在執(zhí)行完 標(biāo)記-整理 之后,由于將“垃圾對象”回收掉了,所以原本連續(xù)使用的內(nèi)存塊便會變得不連續(xù),這樣會導(dǎo)致內(nèi)存塊上面會出現(xiàn)很多小單元的內(nèi)存區(qū)域,這些小單元的內(nèi)存區(qū)域只能夠存放比較小的對象,而比較大的對象是無法直接存儲的。

  • 即原本空閑 1M 的內(nèi)存區(qū)域,有可能會出現(xiàn)無法直接存放 0.9M 大小的對象。


    image.png

標(biāo)記 - 整理算法(Compacting Collector)

  • 上述的 標(biāo)記-清除 算法會產(chǎn)生內(nèi)存區(qū)域使用的間斷,所以為了將內(nèi)存區(qū)域盡可能地連續(xù)使用, 標(biāo)記-整理 算法應(yīng)運(yùn)而生。

  • 標(biāo)記-整理 算法也是由兩步組成,標(biāo)記 和 整理。

  • 第一步的 標(biāo)記 動作也是使用的 根搜索算法,但是在標(biāo)記完成之后的動作卻和 標(biāo)記-清除算法 天壤之別,該算法并不會直接清除掉可回收對象 ,而是讓所有的對象都向一端移動,然后將端邊界以外的內(nèi)存全部清理掉。

  • 該算法所帶來的最大的優(yōu)勢便是使得內(nèi)存上面不會再有碎片問題,并且新對象的分配只需要通過簡單的指針碰撞便可完成。


    image.png

復(fù)制算法(Copying Collector)

  • 無論是標(biāo)記-清除算法還是垃圾-整理算法,都會涉及句柄的開銷或是面對碎片化的內(nèi)存回收,所以,復(fù)制算法 出現(xiàn)了。

  • 復(fù)制算法將內(nèi)存區(qū)域均分為了兩塊(記為S0和S1),而每次在創(chuàng)建對象的時候,只使用其中的一塊區(qū)域(例如S0),當(dāng)S0使用完之后,便將S0上面存活的對象全部復(fù)制到S1上面去,然后將S0全部清理掉。

  • 復(fù)制算法的優(yōu)勢是:① 不會產(chǎn)生內(nèi)存碎片;② 標(biāo)記和復(fù)制可以同時進(jìn)行;③ 復(fù)制時也只需要移動棧頂指針即可,按順序分配內(nèi)存,簡單高效;④ 每次只需要回收一塊內(nèi)存區(qū)域即可,而不用回收整塊內(nèi)存區(qū)域,所以性能會相對高效一點(diǎn)。

  • 但是缺點(diǎn)也是很明顯的:可用的內(nèi)存減小了一半,存在內(nèi)存浪費(fèi)的情況。

  • 所以 復(fù)制算法 一般會用于對象存活時間比較短的區(qū)域,例如 年輕代,而存活時間比較長的 老年代 是不適合的,因?yàn)槔夏甏嬖诖罅看婊顣r間長的對象,采用復(fù)制算法的時候會要求復(fù)制的對象較多,效率也就急劇下降,所以老年代一般會使用上文提到的 標(biāo)記-整理算法。


    image.png

適應(yīng)性算法(Adaptive Collector)

  • 適應(yīng)性算法 其實(shí)不是一種單獨(dú)的回收算法,他只是一種智能選擇回收算法的機(jī)制,也就是該算法會根據(jù)堆內(nèi)存具體的使用情況而自動選用更適合當(dāng)前情況的回收算法。

分代回收

  • 分代回收 并不是一種垃圾回收算法,它是上述各種垃圾回收算法的一個落地應(yīng)用方案。

  • 因?yàn)樯鲜龈鱾€算法都有各自的優(yōu)勢,我們在內(nèi)存的使用過程中,有些對象存活時間長,有些對象存活時間短,有些對象甚至一直存活著,所以根據(jù)對象的存活周期,我們將內(nèi)存區(qū)域分為三大塊:年輕代、老年代 和 永久代,并且年輕代也繼續(xù)細(xì)分為:Eden區(qū)、S0 和 S1。

  • 1、各個內(nèi)存區(qū)域的內(nèi)存大小可以見上文中的內(nèi)存模型圖,當(dāng)然,我們也可以給 JVM 傳遞參數(shù)來進(jìn)行調(diào)整,這些內(nèi)容本文也暫不贅述。

  • 2、 Eden : S0 : S1 的默認(rèn)比例為 8:1:1,為什么這么設(shè)計呢?其實(shí) IBM 有專門的研究表明,年輕代中 98% 的對象都是朝生夕死的,所以只需要劃分為一個較大的 Eden 區(qū)和兩個較小的 Survivor 區(qū)即可,而且這樣做的好處是只有 10% 的 Survivor 區(qū)會被浪費(fèi)掉,這也是可以接受的。
    下面簡單介紹下各個內(nèi)存區(qū)的 GC 過程:

  • 對象首次創(chuàng)建進(jìn)行內(nèi)存分配的時候,首先會放置在 Eden 區(qū),當(dāng) Eden 區(qū)放滿了或者當(dāng)該對象太大無法放進(jìn) Eden 區(qū)的時候,此時會對年輕代(Eden區(qū) 和 S0)進(jìn)行一次 GC,將幸存下來的對象放置在 S1,然后清空掉 Eden區(qū)和 S0 區(qū);(此時年輕代采用的是 復(fù)制算法)
    在上面第一步中對年輕代進(jìn)行垃圾回收的時候,同時會對幸存的對象進(jìn)行標(biāo)記,統(tǒng)計每個幸存對象經(jīng)歷的 GC 次數(shù);

  • 當(dāng) S1 區(qū)滿了之后,或者年輕代的對象經(jīng)歷過指定次數(shù)的 GC 之后,這部分對象會被放置到老年代之中;
    當(dāng)老年代也滿了之后,便會對老年代進(jìn)行一次 GC;(老年代采用的是 標(biāo)記-整理算法)
    垃圾回收器

  • 好了,上文介紹過了 “垃圾”的識別算法 和 “垃圾”的回收算法,那么這些算法的執(zhí)行者是誰呢?就是下文介紹的 垃圾回收器(GC) 了。

垃圾回收器的類型
在 Java 語言中,垃圾回收器按照執(zhí)行機(jī)制來進(jìn)行劃分,主要分為四種類型:

  • 串行垃圾回收器(Serial Garbage Collector);
  • 并行垃圾回收器(Parallel Garbage Collector);
  • 并發(fā)標(biāo)記掃描垃圾回收器(CMS Garbage Collector);
  • G1垃圾回收器(G1 Garbage Collector)。
  • 上述四種垃圾回收器都是有各自的優(yōu)缺點(diǎn)的,我們可以通過向 JVM 傳遞參數(shù)來指定其中一款垃圾回收器。


    image.png

1、串行垃圾回收器(Serial Garbage Collector)

  • 串行垃圾回收器會暫停所有的應(yīng)用程序線程,并采用單獨(dú)的的線程進(jìn)行 GC。

  • 適用于單 CPU、并且對應(yīng)用程序的暫停時間要求不高的情況,所以不太適合當(dāng)前的生產(chǎn)環(huán)境。

2、并行垃圾回收器(Parallel Garbage Collector)

  • 并行垃圾回收器是 JVM 默認(rèn)的垃圾回收器,相較于串行垃圾回收器而言性能稍有提升,它也是需要暫停所有的應(yīng)用程序線程的,但是區(qū)別是它會使用多線程進(jìn)行 GC。

  • 所以并行垃圾回收器適用于多 CPU 的服務(wù)器、并且能接受短暫的應(yīng)用暫停的程序。

3、并發(fā)標(biāo)記掃描垃圾回收器(CMS Garbage Collector)

  • CMS 回收器也是一種并行的垃圾回收器,它會采用多線程來進(jìn)行掃描堆內(nèi)存,標(biāo)記需要清理的對象并將這些對象清理掉。

  • 但是 CMS 它需要更多的 CPU 來保證程序的吞吐量,并且它保證了最短的回收停頓時間,所以,在服務(wù)器允許的情況下,為了達(dá)到更到的性能,我們應(yīng)該使用 CMS 來代替默認(rèn)的 并行垃圾回收器。

4、G1 垃圾回收器(G1 Garbage Collector)

  • G1 垃圾回收器是在 JDK1.7 中才正式引入的一款垃圾回收器,“科技在進(jìn)步,所以一般越是先進(jìn)的技術(shù)一般會更好用并且會替代陳舊的技術(shù)”,好了,玩笑歸玩笑,但是 G1 的引入,目的就是為了取代 CMS 的。

  • 不要被上面 G1 的示意圖誤導(dǎo), G1 并沒有將內(nèi)存進(jìn)行物理劃分,它只是將堆內(nèi)存劃分為一個個的 Region,但是也是屬于分代垃圾回收器,G1 仍然會區(qū)分年輕代和老年代,并且年輕代仍然會有 Eden 區(qū)和 Survivor 區(qū)。

  • 這么做的目的是保證 G1 回收器在有限的時間內(nèi)可以獲得盡可能高的回收效率。

image.png

HotSpot 虛擬機(jī)(HotSpot VM)提供的幾種垃圾收集器

HotSpot VM 提供了 7 種垃圾收集器,分別為:

    1. Serial
    1. PraNew
    1. Parallel Scavenge
    1. Serial Old
    1. Parallel Old
    1. CMS
    1. G1
  • 其中,1、2、3 種適合年輕代內(nèi)存區(qū)的垃圾回收,4、5、6種適合老年代內(nèi)存區(qū)的垃圾回收,并且它們之間是兩兩組合來進(jìn)行使用的,詳見下圖:

image.png

垃圾回收的時機(jī)

  • 垃圾回收分為兩種,F(xiàn)ull GC 和 Scavenge GC。

  • Full GC 發(fā)生在整個堆內(nèi)存中,而 Scavenge GC 僅僅發(fā)生在年輕代的 Eden 區(qū),所以我們應(yīng)該盡可能地減少 Full GC 的次數(shù),當(dāng)然,對于 JVM 的調(diào)優(yōu),很多情況下也是在想辦法對 Full GC 進(jìn)行調(diào)優(yōu)。

因?yàn)?GC 是可能會對應(yīng)用程序造成影響的,所以觸發(fā) GC 也是有一定的條件的,例如:

  • 當(dāng)應(yīng)用程序空閑時,GC 有可能會被調(diào)用,因?yàn)?GC 運(yùn)行線程的優(yōu)先級是相對較低的,所以當(dāng)線程忙的時候,它是不會運(yùn)行的,當(dāng)然,內(nèi)存不足的情況除外;
  • 堆內(nèi)存不足的時候,GC 會被調(diào)用。例如創(chuàng)建對象的時候,若此時內(nèi)存不足,則會觸發(fā) GC 用來給這個對象分配合適的內(nèi)存,當(dāng)進(jìn)行完一次 GC 之后內(nèi)存還是不足,則會繼續(xù)進(jìn)行第二次 GC,若第二次 GC 之后內(nèi)存還是不足,則一般會提示 “out of memory”異常;

小 Tip:
System.gc() 方法會顯示觸發(fā) Full GC,但是它只是對 JVM 的一個 GC 請求,至于何時觸發(fā),還是由 JVM 自行判斷的。

  • GC 的調(diào)用開銷是比較大的,所以我們需要有針對性地進(jìn)行調(diào)優(yōu),一般有如下方案:

    1. 不要顯式調(diào)用 System.gc()。此函數(shù)雖然是建議 JVM 進(jìn)行 GC,但很多情況下它會觸發(fā) GC,從而增加 GC 的頻率;
    1. 盡量減少臨時對象的使用。在方法結(jié)束后,臨時對象便成為了垃圾,所以減少臨時變量的使用就相當(dāng)于減少了垃圾的產(chǎn)生,從而減少了GC的次數(shù);
    1. 對象不用時最好顯式置為 Null。一般而言,為 Null 的對象都會被作為垃圾處理,所以將不用的對象顯式地設(shè)為 Null 有利于 GC 收集器對垃圾的判定;
    1. 盡量使用 StringBuilder 來代替 String 的字符串累加。因?yàn)?String 的底層是 final 類型的數(shù)組,所以 String 的增加其實(shí)是建了一個新的 String,從而產(chǎn)生了過多的垃圾;
    1. 允許的情況下盡量使用基本類型(如 int)來替代 Integer 對象。因?yàn)榛绢愋妥兞勘认鄳?yīng)的對象占用的內(nèi)存資源會少得多;
    1. 合理使用靜態(tài)對象變量。因?yàn)殪o態(tài)變量屬于全局變量,不會被 GC 回收;

其它

  • JVM 的 GC,它就像能看到也能感受到的真實(shí)存在的事物,但是當(dāng)我們?nèi)ド焓謮蛩臅r候,此時它又是虛無縹緲般的存在,處理它的時候還需要格外地謹(jǐn)慎。

  • 因?yàn)樗牟淮_定性,所以我們不應(yīng)該去假定 GC 觸發(fā)的時間,也不要去使用類似 System.gc() 這樣顯示調(diào)用 GC 的方法,這些都是得不償失的。

  • 最需要注意的是我們的編程習(xí)慣和編程態(tài)度,良好的編程習(xí)慣能夠幫助我們規(guī)避掉很多內(nèi)存方面的問題,包括但不僅限于內(nèi)存泄露等。

  • 最后,由于垃圾回收器眾多,在特定的情況下,我們是可以指定使用垃圾回收器的類型的,例如使用:-X:+UseG1GC 來指定使用 G1 垃圾回收器。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容