淺談JVM垃圾回收

JVM內存區域

要想搞懂啊垃圾回收機制,首先就要知道垃圾回收主要回收的是哪些數據,這些數據主要在哪一塊區域。java

Java8和Java8以前的相同點有不少。算法

都有虛擬機棧,本地方法棧,程序計數器,這三個是線程隔離的也稱是線程獨有的;數組

本地內存和堆是線程共享的。安全

Java8和以前JVM內存區域不一樣的是,Java8中增長了元空間,取消了永久代,Java8以前永久代是在堆中的,而以後方法區搬到了元空間中,元空間存在於本地內存中。多線程

下面詳細說一下各個內存區域的特色。併發

  • 虛擬機棧:描述的是方法執行時的內存模型,是線程私有的,生命週期與線程同步,每一個方法被執行的時候都會建立本身的棧幀,主要保存的是局部變量表,操做數棧,動態連接和方法的返回地址等信息。方法執行完成後就清空了棧幀的信息,入棧出棧實際都很明確,而且這塊區域不須要進行GC。
  • 本地方法棧:與虛擬機棧功能很是相似。主要區別是虛擬機棧是爲虛擬機執行java方法,而本地方法棧是爲虛擬機執行本地方法,所以這塊區域也不須要進行GC。
  • 程序計數器:用來記錄每一個線程執行到了哪一條指令。線程隔離的。好比每一個字節碼以前都有一個數字,咱們能夠認爲他就是程序計數器存儲的內容。這些數字的做用就是記錄線程運行時的狀態,方便線程下一次被喚醒的時候能從上次執行的位置繼續執行,須要注意的是程序計數器是惟一一個在Java虛擬機中沒有規定任何OOM狀況的區域,所以這塊區域也不須要進行GC。
  • 堆:對象實例和數組都是在堆上分配的,GC主要對這兩類數據進行回收。
  • 本地內存:線程共享區域。本地內存也叫堆外內存,包含元空間和直接內存。從Java8開始,有了元空間的概念,咱們來看一下爲何要取消永久代,永久代實際上指的是HotSpot虛擬機上的永久代,他用永久代實現了JVM規範定義方法區的功能,永久代主要存放類的信息,常量,靜態變量,即時編譯器編譯後的代碼等,永久代的大小是有限的,能夠經過XX:MaxPermSize參數指定上限,因此若是動態生成類信息或者大量執行String.intern方法(直接將字符串放入永久代)就會形成永久代內存溢出引發OOM。所以在Java8中就將方法區的實現移動到本地內存中的元空間中,這樣方法區就不受JVM的控制了,也就不進行GC,所以有必定的性能提高,一樣這樣方法區也方便在元空間中進行統一管理。

如何識別垃圾

引用計數法

引用計數法就是每一個對象引用你一次,你的對象頭上就+1,若是沒有對象引用你(引用次數爲0),那你涼涼,等着被回收吧。框架

聽着引用計數確實能夠解決咱們沒法識別哪些對象該被回收的問題,可是他還有個主要問題沒被解決,那就是循環引用。什麼是循環引用呢?jvm

例如性能

A a = new Instance("a");
B b = new Instance("b");
a.instance=b;
b.instance=a;
a=null;
b=null;

雖然到最後a和b兩個對象都被置爲null,可是由於他們以前都互相引用過,因此引用的次數都是1,所以沒法被回收。因此現代虛擬機都不使用這種方法來判斷對象是否該回收了。線程

可達性分析算法

現代虛擬機主要是採用這種算法進行判斷獨享是否是該被回收。它的原理是從一個叫作GC Root對象爲起點出發,引出他們指向的下一個節點,再從下一個節點出發,繼續引出下一個,以此類推。這樣就經過GCRoot節點串成了一條引用鏈,若是相關對象不是這個引用鏈上的節點,則會被斷定爲垃圾,而後會被回收。

可達性分析算法能夠解決上述循環引用的問題,由於兩個對象a,b都沒有在GC Root所在的引用鏈上。

對象最後一次垂死掙扎的機會,finalize方法。

當發生GC時,finalize方法給對象一個催死掙扎的機會,當對象可回收的時候,首先會判斷這個對象是否是執行了finalize方法,若是未執行,則會先執行finalize,咱們能夠在finalize方法內部將本對象和GC Root關聯起來,這樣執行完方法後,GC會再次判斷對象是否可被回收,若是可達則不會進行回收。

finalize方法只會執行一次,若是第一次執行方法這個對象變成了可達確實不會回收可是再次對這個對象進行回收的時候,則會忽略finalize方法。

哪些對象能夠做爲GC Root呢?

  • 虛擬機棧中引用的對象(本地變量表中的對象)
  • 方法區中靜態屬性引用的對象。
  • 方法去中常量引用的對象。
  • 本地方法Native中引用的對象。

再談引用

JDK1.2後,Java對引用的概念進行了補充,將引用分爲強引用,軟引用,弱引用,虛引用。強度依次遞減。

  • 強引用:強引用就是new出來的引用,只要強引用存在,垃圾收集器就不會回收掉對象。
  • 軟引用:用來描述一些有用可是未必須的引用,在進行發生內存溢出以前會對軟引用進行回收,若是內存空間充足不會回收軟引用指向的對象,提供了SoftReferemce來實現軟引用。
  • 弱引用也是用來描述非必須對象。可是他的強度比軟引用還要弱,弱引用關聯的對象只能存活到下一次GC以前,不管內存是否充足都會回收弱引用關聯的對象。弱引用用WeakReference類來實現。
  • 虛引用:也叫幽靈引用或者幻影引用,是最弱的一種引用關係,一個對象是否有虛引用的存在徹底不影響對象的生存時間,虛引用存在的目的就是能在這個對象被回收時收到一個系統通知。PhantomReference類來實現虛引用。

垃圾回收算法

上面講了如何經過可達性分析算法來是被哪些數據是垃圾,那具體該經過什麼方式回收垃圾呢?

垃圾回收算法主要由如下幾種方式

  • 標記清除法
  • 複製算法
  • 標記整理法

標記清除法

先用可達性分析算法標記處可回收的對象。

對可回收對象進行回收。

image-20210115122547191

操做簡單不須要移動數據,可是缺點也很明顯,就是存在內存碎片。若是想要再申請的內存空間大小大於碎片的大小就會申請失敗,那要是將回收過的內存區域和原先沒有數據的區域都合併到一塊就能夠了。

複製算法

將堆等分紅兩塊內存區域,咱們暫且把他記做區域A和區域B,A負責分配對象,區域B不分配,A區域中的對象標記爲可回收時,將A中全部不可回收的對象都趕到B中,對A進行統一清除,B中存活的對象緊鄰排列。

這種算法的缺陷也很明顯,我明明堆中還有不少空餘的空間可是不能分配,只能使用一半的空間,另外每次回收都要移動對象,這是很浪費資源而且效率低下。

標記整理法

標記整理法與標記清除法不一樣的是他多了一步整理內存碎片的操做。將全部存活對象都往一端移動,緊鄰排列,再清除另外一端的全部區域,這樣就解決了內存碎片的問題。

可是還有缺點:每次清除可回收對象都要進行對象的移動,效率很低下。

image-20210115123647316

分代收集算法

分代收集算法整合了上面所講的全部算法,綜合以上算法優勢,最大程度避免他們的缺點,所以使現代虛擬機採用的首選算法,於其說他是算法,倒不是說它是一種收集策略。

通過有關專家研究代表,大部分對象(98%)都是朝生夕死,通過一次年輕代的GC就會被回收,因此分代收集算法是根據對象存活週期的不一樣將堆分紅新生代和老年代,在Java8以前還有永久代,新生代和老年代的比例是1:2,新生代又分爲Eden區,from Survivor區,to Survivor區,簡稱S0區和S1區,Eden:S0:S1=8:1:1,咱們將新生代發生的GC叫作Young GC或Minor GC,將老年代發生的GC叫作 Old GC也叫Full GC。

工做原理

新生代的分配和回收

新生代對象通常在Eden區分配,當Eden滿的時候,會發生一次Minor GC,此次Minor GC不多有對象存活,由於大部分對象都是朝生夕死的,少部分存活的對象會被移動到S0區,同時這些對象的年齡+1,最後將Eden區中的全部對象都清除,釋放空間。(複製算法

當發生下一次Minor GC時,會把Eden區中存活的對象和S0中存活的對象都移動到S1,這些對象的年齡+1,同時清空Eden和S0空間。

若再次發生MinorGC重複上面的步驟,只不過此次是將Eden和S1中存活的對象移動到S1,每次Young GC都是S0和S1來回之間移動。由於S0和S1區域比較小,因此下降複製算法頻繁拷貝帶來的開銷。

對象是如何進入到老年代的

大對象直接進入老年代

大對象通常指的是很長的字符串或者數組,當出現大對象時,會致使提早觸發GC,虛擬機提供了一個-XX:PertenureSizeThreshold參數若是對象大小大於這個參數設置的閾值,就認爲是大對象,直接分配到老年代,這樣作的目的是避免Eden和S1,S0區域之間發生大對象的拷貝。

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

虛擬機給每一個對象都定義了一個年齡計數器,每次通過Minor GC後還存活下來的對象,他們的年齡+1,當計數器的值加到必定程度(默認是15),就會晉升到老年代,對象晉升老年代的閾值能夠經過參數-XX:MaxTenuringThershold設置。

動態對象年齡斷定

這種狀況也會晉升到老年代,若是Survivor區中相同年齡的對象大小之和大於Survivor區空間大小的一半,這時候年齡大於等於該年齡的對象也會直接進入老年代,無需和MaxTrnuringThershold參數進行比較。

空間分配擔保

在發生Minor GC以前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部對象的總空間,若是大於,那麼就能夠確保Minor GC是安全的,若是不大於,虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗,若是容許的話,那麼會繼續檢查老年代對象的平均大小,若是大於則進行GC,否者可能進行一次Full GC。儘管空間分配擔保繞的圈子很大,可是平時仍是會開啓擔保的,由於能夠減小Full GC的頻率。

Stop The World

若是老年代滿了,會觸發Full GC,Full GC會同時回收新生代和老年代,也就是對整個堆進行GC,他會致使Stop The World,形成很大的性能開銷。Stop The World就是指在這個GC期間,除了垃圾回收線程在工做,其餘線程會被掛起。

通常Full GC會致使工做線程停頓時間過長,若是再次期間,服務端收到了客戶端不少的請求,則會被拒絕服務,因此纔要儘可能減小Full GC的次數。

所以虛擬機設計成新生代分爲Eden,S0,S1,而且設置對象年齡閾值,默認新生代和老年代的比例是1:2都是爲了不對象過早的進入老年代,儘量晚的觸發Full GC。

老年代採用標記整理法進行垃圾回收。

由於GC都會影響性能,因此咱們要在一個合適的時間點發起GC,這個時間點被稱爲安全點(Safe Point),這個時間點的選定既不能太少讓GC時間太長,也不能過於頻繁以致於過度的增大運行時的負荷,安全點通常是如下特定的位置:

  • 循環的末尾
  • 方法返回前
  • 調用方法的call以後
  • 拋出異常的位置。

垃圾收集器的種類

收集算法實際上是理論層面的,垃圾收集器纔是這些理論具體的實現。

image-20210115134007568

新生代收集器

Serial收集器

Serial收集器收集的是新生代,單線程的垃圾收集器,單線程意味着他只會使用一個CPU或者一個收集線程來進行垃圾回收,他在進行垃圾回收的時候,其餘用戶線程會暫停,在GC期間這個應用不可用。可是在用戶端模式下,他是簡單有效的,對於限定單個CPU的環境來講,Serial單線程模式無需與其餘線程進行交互,較少了開銷,專心作GC能將單線程的優點發揮到極致,在桌面應用場景下,通常不會給虛擬機分配很大的內存,所以STW(Stop The World)的時間會在100ms之內,這點停頓是能夠接受的,因此對於Client模式下的虛擬機,Serial收集器是新生代的默認收集器。

ParNew收集器

ParNew收集器是Serial收集器的多線程版本,除了使用多線程,其餘收集算法以及對象分配,回收策略都和Serial同樣。ParNew主要工做在服務端,服務端若是接受的請求多了,響應時間就很重要,多線程可讓垃圾與回收更快,也就是減小了STW時間,提高響應速度,因此許多運行在服務端的虛擬機採用的新生代垃圾收集器是ParNew ,還有一點,他只能和CMS收集器配合工做,CMS是一個徹底併發的收集器,第一次實現了垃圾收集線程和用戶線程同時工做,採用的是傳統的GC收集器代碼框架,與Serial,ParNew共用一套代碼框架,因此能夠和這兩個收集器配合工做。

在多CPU狀況下,ParNew收集器垃圾收集更快,能夠有效減小STW時間,提高服務端響應速度。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一個使用複製算法,多線程,工做在新生代的垃圾收集器。看起來他的功能和ParNew收集器同樣。可是還有一些不一樣。

關注點不一樣:CMS等垃圾收集器關注的是儘量縮短垃圾收集時用戶線程停頓的時間,而Parallel Scavenge目標是達到一個可控制的吞吐量。
$$
吞吐量=用戶代碼運行時間/(用戶代碼運行時間+垃圾收集時間)
$$
CMS等垃圾收集器更適合用於與用戶交互的應用,提高用戶體驗。而Parallel Scavenge收集器關注的是吞吐量,因此更適合用於後臺運算等不須要太多用戶交互的任務。

Parallel Scavenge收集器提供了兩個參數來精確控制吞吐量,分別是控制最大垃圾手機時間的-XX:MaxGCPauseMillis以及設置吞吐量大小的-XX:GCtimeRatio默認是99%。

除了這兩個參數外,還有第三個參數-XX:UseAdaptiveSizePolicy開啓這個參數後,就不要手工指定新生代大小比例等細節,只須要設置好堆的大小,以及最大垃圾收集時間和吞吐量,虛擬機就會根據當前系統運行狀況動態調整這些參數儘量的達到設定的最大垃圾收集時間和吞吐量,自適應策略是ParallelScavenger和ParNew的重要區別。

老年代收集器

Serial Old

Serial收集器是工做在新生代的單線程收集器。Serial Old是工做在老年代的單線程收集器。這個收集器的主要意義是給Client模式下的虛擬機使用,若是在Server模式下,他還有兩大用途,一種是和JDK1.5以及以前的版本的Parallel Scavenge收集器配合使用,另外一種是做爲CMS的備用方案。

Parallel Old

Parallel Old收集器是相對於Parallel Scavenge收集器的老年代版本,使用多線程和標記整理法。

CMS

CMS收集器是以實現最短STW時間爲目標的收集器,若是應豔紅很重視服務的相應速度,但願給用戶最好的體驗,則CMS收集器是不錯的選擇。

CMS雖然工做在老年代可是回收算法使用的是標記清除法。

一、初始標記

二、併發標記

三、從新標記

四、併發清除

在這四個步驟中,初始標記和從新標記兩個階段會發生STW,形成用戶線程掛起,不過初始標記僅僅標記GC Root可以關聯的對象,速度很快,從新標記是進行GC Root跟蹤引用鏈的過程,是爲了修正併發標記期間由於用戶線程繼續運行而致使標記產生變更的哪一部分對象的標記記錄,這一階段停頓時間通常比初始標記更長,但比並發標記短。

整個過程執行時間最長的是併發標記和標記整理,不過這兩個階段用戶線程均可以工做,因此不影響應用的正常使用,因此整體上看,能夠認爲CMS是內存回收線程和用戶線程一塊兒併發執行的。

可是他有三個缺點:

  • CMS收集器對CPU資源很是敏感。好比原本有10個用戶線程處理請求,如今要分出三個線程作垃圾回收工做,吞吐量降低了30%,CMS默認啓動的回收線程數=(CPU數量+3)/4,若是CPU是2個,那麼吞吐量直接下降50%。顯然是不可接受的。
  • CMS沒法處理浮動垃圾,什麼是浮動垃圾?由於併發清理階段,用戶線程還在工做,因此還會出現新的可回收對象,這部分垃圾只能在下一次GC時再清理,因此這部分垃圾就是浮動垃圾。由於垃圾收集階段用戶線程還在運行因此須要預留足夠多的空間確保用戶線程正常執行,這就意味着CMS收集器要提早進行Full GC,JDK1.5默認當老年代使用68%空間就後被激活,這個比例能夠經過-XX:CMSInitiatingOccupancyFraction來設置,可是若是設置過高容易致使CMS運行期間預留的內存不夠,致使Concurrent Model Failure,這時會啓用Serial Old收集器進行老年代的收集工做,可是Serial old 是單線程的,這就致使STW時間更長了。
  • CMS由於採用的是標記清除法,因此會存在大量的內存碎片,若是沒法找到足夠的內存空間進行分配,就會觸發FUllGC進行垃圾回收,影響應用的性能,咱們能夠開啓-XX:+UseCMSCompactAtFullCollection,這個參數是當CMS頂不住要進行Full GC時開啓內存碎片的合併整理過程,內存整理會致使STW,停頓時間會變長,還能夠用另外一個參數-XX:CMSFullGCsBeforeCompation用來設置執行多少次不壓縮的Full GC事後再進行一次壓縮。

G1(Garbage First)

G1收集器歐式面向服務端的垃圾收集器,被稱爲駕馭一切的垃圾回收器。

特色以下:

  • 向CMS收集器同樣,能與應用程序線程併發執行。
  • 整理空閒空間更快。
  • 須要GC停頓時間更好預測。
  • 不會像CMS那樣犧牲大量的吞吐性能。
  • 不須要打的java 堆。

與CMS相比,它有如下方面表現得更爲出色。

  1. 運行期間不會產生內存碎片。總體採用標記整理法,局部採用複製算法,兩種算法都不會產生內部碎片。
  2. STW創建在可預測的停頓時間模型,用戶能夠指按期望停頓時間,G1將會停頓時間控制在用戶設定的停頓時間之內。

他爲何能創建可預測模型呢?

主要緣由是他和傳統的內存分配存儲方式不同。傳統內存分配是連續的,新生代,老年代。可是G1的存儲地址不是連續的,每一代都是用N個不連續的大小相同的Region,每一個Region佔有一塊連續的虛擬內存地址。和傳統相比還多了一個H區,表明Humongous,標會存儲的是大對象。當對象大小大於Region的通常,就直接分給老年代,防止GC時反覆拷貝大對象。

這樣作G1就能夠根據Region的價值大小(回收所得到的空間大小以及回收經驗值)進行排序,維護成一個優先級列表,根據容許的時間,回收截止最大的Region,也就避免了整個老年代的回收,減小了STW形成的停頓時間。

G1收集器工做步驟

  1. 初始標記
  2. 併發標記
  3. 最終標記
  4. 篩選回收

篩選階段會根據各個Region的回收價值和成本進行排序,根據用戶指望的GC停頓時間來制定回收計劃。

相關文章
相關標籤/搜索