天天看點

JVM學習筆記(二):垃圾回收

文章目錄

    • 1、如何判斷對象是否可以回收
      • 1.1、引用計數法
      • 1.2、可達性分析算法
      • 1.3、四種引用
    • 2、垃圾回收算法
      • 2.1、标記清除算法
      • 2.2、标記整理算法
      • 2.3、複制算法
    • 3、分代垃圾回收機制
    • 4、垃圾回收器(7種)
      • 4.1、串行回收器(Serial + SerialOld)
      • 4.2、并行回收器(Parallel + ParallelOld)
      • 4.3、響應時間優先(ParNew + CMS)
      • 4.4、G1回收器
        • 4.4.1、G1回收階段
        • 4.4.2、各階段工作流程
        • 4.4.3、Young Collection 跨代引用
        • 4.4.4、重标記 Remark

1、如何判斷對象是否可以回收

1.1、引用計數法

我們的對象是存放在堆空間當中,判斷對象是否可以被回收,這裡有兩種不同的算法。

我們首先講解第一種,引用計數法:

隻要一個對象被其他變量所有引用,就讓該對象的計數 + 1,如果被引用了兩次就讓其計數變為2

如果某個變量不再引用它,計數就會 - 1,那麼這個對象的引用計數變為0時,意味着沒有變量對該對象進行引用

它就會作為一個垃圾,進行回收

但是該方式有一個弊端:循環引用!

JVM學習筆記(二):垃圾回收

如圖所示,A對象引用B,此時B計數為1,B又引用了A,此時A計數變為1。這兩個對象互相引用,沒有别的對象引用它們,此時他們兩個是否會被垃圾回收? 不會!

因為他們各自的引用計數是 1,雖然它倆都不會被使用,但是它們的引用計數不是0,是以不能被回收,這樣就造成記憶體洩漏

引用計數法是在Python虛拟機中采用,然而Java虛拟機并沒有采用該方法進行判斷,JVM采用的是可達性分析算法

1.2、可達性分析算法

該算法是JVM采用判斷方法,該算法首先需要确定根對象(GC Root),什麼是根對象?

可以了解為肯定不能當成垃圾回收的對象

在垃圾回收之前,我們首先會對堆記憶體中所有對象進行掃描,看看對象是否直接或間接被根對象(GC Root)進行引用

如果是就不能被回收,反之就表示可以作為垃圾将來被回收

1.3、四種引用

面試時候會經常問道四種引用,例如:Java中的四種引用有哪些?

嚴格來說,并不是隻有四種,而是有五種,這裡我們會介紹五種:

  1. 強引用
  2. 軟引用
  3. 弱引用
  4. 虛引用
  5. 終結器引用
JVM學習筆記(二):垃圾回收

注意:實線表示強引用

其實我們平時使用的所有引用都是強引用,比如我們new一個對象,此時變量就強引用了new出來得對象。

強引用的特點:就是沿着GC Root引用鍊找到對象,那麼該對象就不會被垃圾回收,隻有GC Root對該對象的引用斷開,才會被垃圾回收

軟引用和弱引用比較相似,跟強引用的差別是:

隻要A2和A3沒有被直接強引用,此時垃圾回收發生時,它們都可能被回收
例如:A2對象被C對象間接地強引用,同時被B對象進行強引用,這種情況不會被垃圾回收,當B對象不在對A2對象強引用,隻有軟引用引用A2對象,發生垃圾回收并且記憶體不夠,此時就會把軟引用引用的對象釋放掉
弱引用跟軟引用很像,沒有強引用直接引用時,并且隻要發生垃圾回收(不管記憶體是否不足) ,都會把弱引用的對象回收!

軟引用和弱引用可以配合引用隊列一起工作,當軟引用的對象被回收,軟引用自身也是一個對象,如果在建立時給它配置設定了一個引用隊列,當軟引用所引用的對象被回收時,軟引用本身就會進入到隊列,弱引用也是同理

為什麼要做這樣的處理?

因為不論是軟引用還是弱引用,他們自身也要占用一定的記憶體,如果想要對他倆占用的記憶體做進一步釋放,需要使用引用隊列來找到他倆,做進一步處理,畢竟他倆還被C對象引用着。

接下來介紹一下虛引用和終結器引用,它倆與軟引用、弱引用不同:

JVM學習筆記(二):垃圾回收

軟引用和弱引用既可以跟引用隊列一起使用,也可以不一起使用

而虛引用和終結器引用必須和引用隊列一起使用!

是以虛終引用被建立時候,就會關聯引用隊列

當我們建立ByteBuffer實作類對象時,就會建立一個名為cleaner的虛引用對象,ByteBuffer會配置設定一塊直接記憶體,并且把記憶體位址傳遞給虛引用對象,為什麼要這麼做?

将來ByteBuffer一旦沒有強引用所引用,此時ByteBuffer将被垃圾回收,但是它配置設定的直接記憶體無法被Java垃圾回收管理,是以我們需要在ByteBuffer被回收時,讓虛引用對象進入引用隊列中。虛引用所在的隊列會由一個叫做ReferenceHandler線程定時的到引用隊列中尋找,看有沒有新入隊的Cleaner,如果有就會調用clean方法,該方法會根據前面記錄的直接記憶體位址調用

Unsafe.freeMemory()

,這樣才能把我們的直接記憶體釋放掉!就不會導緻直接記憶體洩露,這就是虛引用的作用

我們再來解讀一下終結器引用:

我們都知道,所有的Java對象都會繼承Object類,該類中有一個叫做

finallize()

終結方法,當我們的A4對象重寫了該方法,并且沒有強引用,此時就可以當作垃圾進行回收。

那麼該方法何時被調用?

當沒有強引用引用A4對象時,此時JVM會建立終結器引用,當A4對象将要被垃圾回收時,終結器引用會加入到引用隊列中,此時A4還沒有被垃圾回收,此時由一個優先級很低得線程

FinallizeHandler

檢視引用隊列中是否有終結器引用,如果有就會根據終結器引用找到需要垃圾回收的對象A4,并且調用A4重寫的

finallize()

,第二次垃圾回收時就會将A4對象進行回收,這就是終結器引用的作用

我們發現,

finallize()

工作效率很低,第一次回收時并不能直接釋放,而是先要将終結器對象入隊,而且處理隊列的

FinallizeHandler

線程優先級很低,執行的機會很少,是以會造成該方法遲遲不能調用,對象占用的記憶體遲遲不能釋放,這就是為什麼不推薦使用

finallize()

釋放資源的理由

2、垃圾回收算法

具體如何進行垃圾回收,是需要垃圾回收算法,常見有三種:

  1. 标記清除
  2. 标記整理
  3. 複制

2.1、标記清除算法

JVM學習筆記(二):垃圾回收

标記清除算法分為兩個階段:

  1. 先标記,看看堆記憶體中哪些對象可以作垃圾(沒有GC Root直接引用)
  2. 清除,把堆記憶體中垃圾對象所占用的對象給清除,隻需要把對象所占用記憶體的起始、結束位址,放到空閑的位址清單中,下次配置設定對象時,就去位址列中檢視是否有空閑的空間存放新對象

标記清除算法的優缺點:

  • 優點是速度快,隻需要把垃圾對象起始、結束位址做記錄,不需要做額外處理,是以回收速度很快
  • 缺點是容易産生記憶體碎片,因為清除之後不會對空閑的記憶體空間做進一步整理工作,當存入的對象占用記憶體過大,由于記憶體碎片的特點不連續,導緻沒辦法直接将對象存入,會造成記憶體溢出

2.2、标記整理算法

JVM學習筆記(二):垃圾回收

标記整理和标記清除在第一個階段是一樣的,差別主要是整理部分,

整理操作,避免之前出現的記憶體碎片問題,在清理過程中把不回收的對象向前移動,讓記憶體空間緊湊,這樣就沒有标記清除算法中的缺點

優缺點:

  • 優點:沒有記憶體碎片
  • 缺點:由于整理過程牽扯到對象的移動,效率自然會變低,如果有些變量引用了我們移動的對象,此時引用位址會發生改變,會降低性能

2.3、複制算法

JVM學習筆記(二):垃圾回收

複制算法是将記憶體區域劃分為大小相等的兩塊,分别是FROM和TO,TO區域是空閑的

首先還是進行垃圾對象标記,然後将FROM區不被回收的對象複制到TO區域中,複制過程中會完成記憶體碎片整理。複制完成後FROM區都是垃圾對象,将所有垃圾對象全部清空,并且交換 FROM和TO區域位置。

複制算法優缺點:

  • 優點:不會産生記憶體碎片
  • 缺點:占用雙倍記憶體空間

3、分代垃圾回收機制

分代垃圾回收機制會将堆記憶體劃分為兩部分:

  • 新生代
  • 老年代

新生代劃分為:

  • 伊甸園
  • 幸存區FROM
  • 幸存區TO

為什麼要做區域劃分?

主要是因為Java中有的對象需要長時間使用,這類對象存放到老年代中

那些使用完可以被回收的對象就放入到新生代中

這樣就可以針對對象生命周期的特點進行不同的垃圾回收政策

老年代垃圾回收很久發生一次,新生代垃圾會收很頻繁

不同區域采用不同算法,就會更有效管理垃圾回收區域

JVM學習筆記(二):垃圾回收

當我們建立新的對象時,預設采用伊甸園記憶體空間,當我們建立多個對象,伊甸園放不下了,此時就會觸發一次垃圾回收,新生代的垃圾回收我們稱之為

Minor GC

,觸發後采用可達性分析算法判斷哪些對象可以作為垃圾進行标記,标記成功後就會采用複制算法,把存活的對象複制到幸存去TO,然後将幸存的對象壽命 + 1,做完複制操作後會交換FROM和TO的位置。

重複上述操作伊甸園又滿了,就觸發第二次垃圾回收,不僅要判斷伊甸園中對象是否需要回收,就連幸存區的對象也要進行判斷是否需要進行回收。

幸存區中對象并不會永遠存放于幸存區,當它的壽命超過預設門檻值15,說明該對象價值很高,經常在使用,這樣就沒必要繼續放到幸存區了,就會把該對象晉升到老年代中。因為老年代垃圾回收頻率低,這種價值較高的對象就會從幸存區晉升到老年代。

有這麼一種情況:

晉升到老年代對象很多,新生代也快滿了,此時新生代還要存入一個對象,此時存不下了。

這時就會觸發

Full GC

,觸發垃圾回收,從新生代到老年代做一個整個垃圾回收操作,這就是基本流程

總結:

  • 對象首先配置設定在伊甸園區域
  • 新生代空間不足時,觸發 minor gc,伊甸園和 from 存活的對象使用 copy 複制到 to 中,存活的

    對象年齡加 1并且交換 from to

  • minor gc 會引發 stop the world,暫停其它使用者的線程,等垃圾回收結束,使用者線程才恢複運作
  • 當對象壽命超過門檻值時,會晉升至老年代,最大壽命門檻值是15(4bit)
  • 當老年代空間不足,會先嘗試觸發 minor gc,如果之後空間仍不足,那麼觸發 full gc,世界暫停(stop the world)的時間更長

特殊情況:當我們向新生代存放一個大對象,此時記憶體不夠,即使發生了垃圾回收記憶體也不夠直接存放大對象,如果老年代空間足夠,這時就會把大對象直接晉升到老年代當中。

4、垃圾回收器(7種)

垃圾回收器可以分為三類:

  1. 串行
  2. 吞吐量優先
  3. 響應時間優先

特點:

串行:
  • 單線程
  • 堆記憶體較小,單核CPU
吞吐量優先:
  • 多線程
  • 堆記憶體較大,需要多核CPU來支援
  • 讓機關時間内,STW時間最短
響應時間優先:
  • 多線程
  • 堆記憶體較大,需要多核CPU來支援
  • 盡可能讓單次STW時間最短

4.1、串行回收器(Serial + SerialOld)

JVM學習筆記(二):垃圾回收

串行垃圾回收器開啟語句如圖,該垃圾回收器分為兩部分:

  1. Serial:工作在新生代,采用的回收算法是複制,新生代記憶體不足發生垃圾回收使用Serial完成Minor GC
  2. SerialOld:工作在老年代,采用的算法是标記整理,老年代記憶體不足發生垃圾回收使用SerialOld完成Full GC

回收過程:

假設現在有多核CPU,如圖所示。剛開始4個線程都在運作。這時發現堆記憶體不夠,觸發垃圾回收,首先要讓這些線程在安全點停下,因為在垃圾回收過程中,可能對象的位址需要發生改變。為了保證安全使用這些對象的位址,需要先讓線程達到安全點暫停,此時完成垃圾回收工作,就不會有其他線程進行幹擾。

注意:由于

Serial

SerialOld

都屬于單線程垃圾回收器,是以隻有一個垃圾回收線程在運作,當垃圾回收線程運作時,其他線程全部進行阻塞暫停。如果垃圾回收線程結束,其他使用者線程恢複運作。

4.2、并行回收器(Parallel + ParallelOld)

JVM學習筆記(二):垃圾回收

使用吞吐量優先的并行垃圾回收器需要使用圖中第一條參數進行開啟,這兩個開關在1.8中預設是開啟的。

UseParallelGC

是新生代垃圾回收器,采用複制算法,

UseParallelOldGC

是老年代垃圾回收器,采用标記整理算法。單從算法上來看,跟我們之前介紹的串行垃圾回收器是一樣的。

差別在于

Parallel

這個名詞(并行),暗指這兩個垃圾回收器都是多線程

值得一提的是,這兩個隻要開啟其中一個,就會順帶把另一個進行開啟。

回收過程:

如圖所示,現在有多核CPU,共四個線程都在運作。突然記憶體不足,觸發一次垃圾回收,這些使用者線程就會在安全點停下來。垃圾回收器會開啟多個線程進行垃圾回收,垃圾回收線程的個數預設和CPU數量一緻。

線程數可以通過第五條指令來控制。打開第二條指令時,吞吐量優先回收器工作時,就會動态的調整伊甸園跟幸存區的比例,包括整個堆空間大小、晉升門檻值都會進行調整。

4.3、響應時間優先(ParNew + CMS)

JVM學習筆記(二):垃圾回收

開啟參數在第一行,

UseConcMarkSweepGC(CMS)

是基于标記清除的垃圾回收器,工作于老年代,并且是并發的,并發是指垃圾回收器工作時,其他的使用者線程也能同時進行,使用者線程和垃圾回收線程是并發執行。

并行的含義是指垃圾回收器運作期間,工作線程不能運作(STW)。

CMS垃圾回收器某些時刻會出現并發效果。與之配合的

UseParNewGC

是工作于新生代垃圾回收器。

CMS垃圾回收器有些時刻會發生并發失敗問題,此時會采取補救措施,CMS回收器會退化成SerialOld單線程垃圾回收器。

工作流程:

多個CPU并行執行,此時老年代發生記憶體不足,線程們都會到達安全點暫停,然後執行CMS垃圾回收器,CMS會執行初始标記動作,仍然需要STW,隻會标記根對象,标記完成後使用者線程恢複運作,與此同時垃圾回收線程進行并發标記,把剩餘垃圾标記出來,此時跟使用者線程是并發執行的,不需要STW,是以響應時間是很短的,并發标記後還要進行重複标記,為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,又要STW。因為并發标記時使用者線程也在工作。重新标記結束後使用者線程又可以恢複運作,最後垃圾回收線程做并發清理。

細節:

  • 初始标記時,線程數受到途中第二條參數影響,根據我們的例子,n為4
  • 但是并發的GC線程數不一樣,建議把第二條第二個參數設定為并行線程數的1/4
  • CMS垃圾回收器工作過程中,執行并發清理,由于其他使用者線程還可以繼續執行,此時運作過程中可能産生新的垃圾,是以并發清理同時不能把新的垃圾幹掉,是以等到下一次垃圾回收清理。這些新垃圾我們把它叫做浮動垃圾,需要等到下次做垃圾回收時才能清理掉。是以我們需要預留白間儲存浮動垃圾,我們途中第三條參數就是用來控制何時進行CMS垃圾回收時機。如果

    percent

    指派為80,那就是當老年代記憶體占用達到80%,就執行一次垃圾回收,為了預留白間給浮動垃圾
  • 在我們重新标記階段有一個特殊的場景,有可能新生代對象會引用老年代對象,如果此時進行重新标記,必須掃描整個堆記憶體,這樣對性能影響非常大。 我們可以使用最後一條參數避免這種情況,在做重新标記之前,先對新生代做垃圾回收,這樣新生代存活對象少了,将來掃描的對象就少了,這樣就會減輕重新标記時壓力
  • CMS有個特點:在記憶體碎片較多情況下,會造成将來配置設定對象時新生代和老年代空間都不足,這樣就會造成并發失敗,此時CMS老年代垃圾回收器不能正常工作,此時CMS會退化為SerialOld,做一次單線程串行垃圾回收進行整理,這樣碎片減少了才能進行工作

4.4、G1回收器

使用場景:

  • 同時注重高吞吐和低延遲,預設暫停目标是200ms

    超大堆記憶體,會将堆劃分為多個大小相等的

    Region

    整體上是标記整理算法,兩個區域之間是複制算法

4.4.1、G1回收階段

JVM學習筆記(二):垃圾回收

G1垃圾回收分為三個:

  1. Young Colletion:新生代垃圾收集
  2. Young Colletion + Concurrent Mark:新生代垃圾收集 + 并發标記
  3. Mixed Colletion:混合收集

這三個階段是個循環過程,最開始是新生代垃圾收集,當老年代記憶體超過門檻值,會在新生代垃圾收集的同時進行并發标記,該階段完成之後會進行混合收集,會對新生代幸存區和老年代都來進行規模較大的收集,混合收集結束後,再次進入新生代垃圾收集過程…

4.4.2、各階段工作流程

Young Colletion
JVM學習筆記(二):垃圾回收

G1垃圾回收器會把整個堆記憶體劃分為無數等大區域,每個區域都可以獨立作伊甸園、幸存區、老年代。

剛開始區域都是空閑的,新建立的對象會配置設定到伊甸園(E),當伊甸園區逐漸被占滿就會觸發新生代的垃圾回收,觸發STW

JVM學習筆記(二):垃圾回收
新生代垃圾回收後,會把幸存的對象複制算法放入幸存區(S)
JVM學習筆記(二):垃圾回收
當幸存區對象也比較多,或者存存貨年齡超過門檻值,此時又會發生垃圾回收,幸存區部分對象會晉升到老年代,不夠年齡的會複制到另一個幸存區
Young Colletion + CM
JVM學習筆記(二):垃圾回收

我們進行垃圾回收時,會将對象進行初始标記和并發标記,初始标記就是标記根對象,并發标記是從根對象出發順着引用鍊找到其他标記對象。

初始标記在新生代GC時就發生了,并發标記是當老年代占用堆空間比例達到一定的門檻值會發生并發标記

Mixed Colletion
JVM學習筆記(二):垃圾回收

該階段,伊甸園區對象會通過複制算法複制到幸存區中,包括不夠年齡的幸存區對象也會複制到幸存區,符合晉升條件的對象會晉升到老年代區

還有一部分老年代的區域,發現裡面一些對象沒用了,老年代也采用複制算法複制到新的老年代區域(紅色箭頭),圖中參數會根據最大暫停實際按有選擇的進行回收。因為堆記憶體空間太大了,老年代垃圾回收時間可能比較長,達不到最大暫停時間這個目标。為了達到這個目标,G1就會從老年代選出回收價值最高的區進行垃圾回收,這樣就能達到暫停時間。

是以混合收集階段,會優先收集垃圾最大的區域,目的就是達到暫停時間短的目的。

4.4.3、Young Collection 跨代引用

JVM學習筆記(二):垃圾回收

首先找到根對象,根對象進行可達性分析找到存活對象,存活對象進行複制到幸存區。

根對象有一部分來自于老年代,老年代存活對象很多,如果周遊整個老年代效率很低。是以将老年區再次細分,每個card大約是512k,如果老年代有一個對象引用新生代對象,對應的card标記為dirty card,這樣就不需要周遊整個老年代了,縮小了搜尋範圍。

JVM學習筆記(二):垃圾回收

該圖中粉色區域為dirty card區,新生代這邊有Remeber Set,會記錄外部引用(都有哪些髒卡),将來對新生代進行垃圾回收時,就可以通過Remeber

Set 知道對應了那些髒卡,然後去髒卡區周遊GC Root

這裡有個問題:我們需要标記髒卡,但是當引用發生變更時都要去更新髒卡,這是異步操作,不會立刻完成髒卡更新。會把更新指令放入到dirty card queue中,将來由線程完成髒卡更新操作!

4.4.4、重标記 Remark

JVM學習筆記(二):垃圾回收

上面總是提到過并發标記、重新标記這兩個名詞階段,說白了就是Remark階段,接下來介紹一下Remark階段相關知識

上面這個圖表示并發标記階段時對象的處理狀态。

圖中黑色表示已經處理完成的,并且被引用了,說明黑色不會被垃圾回收。

灰色表示正在進行處理,白色是尚未處理的。灰色如果最終被處理完成,他就會最終變成黑色。右下角那個白色由于也有引用,是以它最終也會變成黑色存活下來。

思考個問題:

JVM學習筆記(二):垃圾回收

如果處理到B,因為有強引用,是以就變成黑色。如果處理到C,由于我們這個标記是并發,意味着同時也有使用者線程将對象引用就行修改,比如将B和C之間強引用斷開,此時處理完B處理C,發現C和B之間已經沒有聯系了,處理到C進行标記,C為白色。

真個并發标記結束之後,C對象仍然是白色,會被回收掉,這是第一種情況

JVM學習筆記(二):垃圾回收
JVM學習筆記(二):垃圾回收

另一種情況:在C和B處理完之後,并發标記可能還沒結束,使用者線程又改變了C位址,比如說把C對象當成A對象的屬性,做一次指派操作。因為C之前處理過了,認為已經是白色的。等到整個并發标記結束後,仍然認為C是垃圾,就會被回收了,這樣就不對了,因為C被強引用了,不該被回收。

是以需要對對象的引用做進一步檢查,就是我們提到的

Remark

重新标記階段,防止該現象的發生。

具體操作:當對象引用發生改變,JVM就會加入一個寫屏障,隻要C對象引用發生改變,寫屏障功能就執行,會把C加入到隊列中,把C變成灰色,整個并發标記結束,進入重新标記階段,STW。重新标記線程就會把隊列中對象取出來再次檢查,發現是灰色的,要對它做進一步判斷處理,發現強引用,是以還是要把它變成黑色。這樣C不會被誤當成垃圾回收掉