天天看點

jvm中GC知識詳細講解

一、 JVM垃圾回收算法與配置設定政策

在Java的運作時資料區中,程式計數器、虛拟機棧、本地方法棧這三個區域都是線程私有的,跟着線程一起建立,當線程或者方法結束後,記憶體也被回收。但是Java堆和方法區不一樣,這部分記憶體是動态配置設定和回收的,垃圾回收器關注的是這部分記憶體。

GC主要弄明白以下三個問題:

  • 那些記憶體需要被回收?
  • 什麼時候回收?
  • 如何進行回收?

那些記憶體需要被回收?

垃圾回收首先要判斷那些對象是可以被回收的,有兩種垃圾回收算法:

  • 引用計數法:給對象添加一個引用計數器,每當有一個地方引用時,計數器+1,當引用失效時,計數器-1;當計數器為0時,被回收。但是這種方法不能解決對象之間循環引用的問題
  • 可達性分析算法(GC ROOT):通過一系列GC ROOTS對象作為起點,向下周遊,能夠被周遊到的對象認為可以存活,沒有被周遊到的需要被回收。

在Java種,可以作為GC Roots對象包括以下幾種:

  • 虛拟機棧(棧幀中的局部變量表,Local Variable Table)中引用的對象。
  • 方法區中類靜态屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

兩次标記與finalize()方法

即使在可達性分析算法中不可達的對象,也不是一定會死亡的,他們都會處于一種緩刑階段,要讓一個對象真正被回收,至少需要經曆兩次标記過程:

如果對象在進行可達性分析後,發現沒有與GC Roots相連接配接的引用鍊,則該對象第一次标記,并且進行篩選,篩選的條件是此對象是否有必要執行finaliza()方法,當對象沒有此方法或者被調用過後,虛拟機都會認為沒有必要執行。如果有必要執行,會将此對象放置在一個F-Queue隊列中,飯後建立一個低優先級的線程去執行它。

什麼時候開始回收?

對于Minor GC,當Eden空間滿了,就會觸發一次Minor GC。但是Full GC相對複雜,而且耗時長,JVM調優也是較少Full GC的次數。其觸發條件大緻以下幾種:

  • 調用System.gc():此方法建議JVm進行Full GC,但是不一定會執行。
  • 老年代空間不足:當大對象直接進入老年代,長期存活的對象進入老年代等,執行完Full GC後空間任然不足,此時會抛出 Java.lang.OutOfMemoryError: Java heap space。
  • 空間配置設定擔保失敗:使用複制算法的Minor GC需要老年代空間作為擔保,如果擔保失敗,則會觸發Full GC

如何進行回收?

主要有四種垃圾回收算法:标記 - 清除、标記 - 整理、複制算法、分代回收算法

标記 - 清除算法

此算法分成标記、清除兩個階段:首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。

标記 - 清除算法的缺點:

  • 空間問題:标記清除後會産生大量的不連續記憶體碎片,當太多可能會在配置設定較大對象時因為沒有連續的空間而觸發GC
  • 效率問題:因為記憶體碎片,操作更加費時

示意圖如下:

jvm中GC知識詳細講解

複制算法

為了解決标記 - 清除算法的效率問題,出現了一種複制算法:将可用記憶體按容量分成大小相等的兩塊,每次隻使用其中一塊,當這一塊記憶體使用完,久将還存活的對象複制到另一塊上面,然後将已經用過的這一塊全部清除。

這樣可以每次都是對整個半區進行記憶體回收,不需要考慮記憶體碎片等情況。但是缺點是将可用記憶體縮小為原來的一半。執行過程如圖:

jvm中GC知識詳細講解

Minor GC過程:新生代将記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次Minor GC都會将Eden和From Survivor中還存活的對象一次性的複制到另外一塊To Survivor空間上,然後清理掉Eden和剛使用過的Survivor空間。當對象經曆15次GC還存活後,将會轉入老年區。在每次Minor GC結束後,Eden空間是空的,兩個Survivor空間其中一個是空的,另一個存儲着存活的對象。

配置設定擔保:

當Minor GC進行記憶體回收時,如果另外一塊Survivor上沒有足夠空間存放上一次新生代收集下來的存活對象,這些對象将直接通過配置設定擔保機制進入老年代。如果老年代配置設定擔保不足,會觸發full GC

标記 - 整理算法

複制算法在對象存活率較高的時候需要進行較多的複制操作,效率就會變低。根據老年代的特點,标記 - 整理算法被提出來:此算法魚的标記過程與标記 - 清除算法一樣,但是後面是清除之後對對象進行整理,保證沒有記憶體碎片。如圖:

jvm中GC知識詳細講解

分代收集算法

主要思想:根據對象存活周期的不同将記憶體劃分為不同區域,一般Java将堆分為新生代和老年代,這樣可以根據不同區域選擇不同的收集算法:

  • 新生代:每次垃圾收集都有大量對象死去,隻有極少數存活,選擇複制算法。
  • 老年代:對象存活率高,沒有額外的空間進行擔保,就必須使用标記 - 清除或者标記 - 整理算法來進行回收。

記憶體配置設定政策

對象優先在Eden區配置設定

大多數情況下,對象在新生代的Eden區中配置設定,當Eden區沒有足夠的空間進行配置設定時,虛拟機将發起一次Minor GC

大對象直接進入老年代

大對象指需要大量連續的記憶體空間的Java對象。經常出現大對象很容易導緻記憶體還有不少空間就提前觸發GC以擷取足夠的連續空間來存儲大對象。

虛拟機提供了一個-XX:PretenureSizeThreshold參數,令大于這個設定值的對象直接在老年代配置設定。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複制(新生代采用複制算法回收記憶體)

長期存活的對象進入老年代

虛拟機給每個對象定義了一個對象年齡(Age)計數器。如果對象在Eden出生并經過第一次Minor GC後仍然存活,并且能被Survivor容納的話,将被移動到Survivor空間中,并且對象年齡設為1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲),就将會被晉升到老年代中。

二、 垃圾收集器

如果說收集算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實作。在1.8垃圾收集器主要有以下幾種:

jvm中GC知識詳細講解

上圖7中作用于不同代的收集器,如果兩個收集器之間有着連線,則可以搭配使用。

并行和并發

  • 并行(Parallel):指多條垃圾收集線程并行工作,但此時使用者線程仍然處于等待狀态。
  • 并發(Concurrent):指使用者線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),使用者程式在繼續運作。而垃圾收集程式運作在另一個CPU上。

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,是以Minor GC非常頻繁,一般回收速度也比較快。具體原理見上一篇文章。
  • 老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了Major GC,經常會伴随至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集政策裡就有直接進行Major GC的政策選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

新生代收集器

Serial收集器:

jvm中GC知識詳細講解

采用複制算法的新生代收集器,是一個單線程的,在進行垃圾收集時,必須暫停其他所有的工作,直至收集器結束。

ParNew收集器:

ParNew收集器就是Serial收集器的多線程版本,新生代收集器,采用多線程進行垃圾收集

ParNew收集器除了使用多線程收集外,其他與Serial收集器相比并無太多創新之處,但它卻是許多運作在Server模式下的虛拟機中首選的新生代收集器,其中有一個與性能無關的重要原因是,除了Serial收集器外,目前隻有它能和CMS收集器(Concurrent Mark Sweep)配合工作

Parallel Scavenge 收集器

jvm中GC知識詳細講解

并行的多線程新生代收集器,使用複制算法。Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能縮短垃圾收集時使用者線程的停頓時間,而Parallel Scavenge收集器的目标是達到一個可控制的吞吐量,它不能與CMS收集器配合使用。

老年代收集器

Serial Old收集器

jvm中GC知識詳細講解

Serial Old 是 Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“标記-整理”(Mark-Compact)算法。

Parller Old收集器

jvm中GC知識詳細講解

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“标記-整理”算法。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器,它非常符合那些集中在網際網路站或者B/S系統的服務端上的Java應用,這些應用都非常重視服務的響應速度。從名字上(“Mark Sweep”)就可以看出它是基于“标記-清除”算法實作的。

CMS收集器工作的整個流程分為以下4個步驟:

  • 初始标記(CMS initial mark):僅僅隻是标記一下GC Roots能直接關聯到的對象,速度很快,需要“Stop The World”。
  • 并發标記(CMS concurrent mark):進行GC Roots Tracing的過程,在整個過程中耗時最長。
  • 重新标記(CMS remark):為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段稍長一些,但遠比并發标記的時間短。此階段也需要“Stop The World”。
  • 并發清除(CMS concurrent sweep)

G1收集器

jvm中GC知識詳細講解

在G1之前的其他收集器進行收集的範圍都是整個新生代或者老生代,而G1不再是這樣。G1在使用時,Java堆的記憶體布局與其他收集器有很大差別,它将整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是實體隔離的了,而都是一部分Region(不需要連續)的集合。