(轉)JVM垃圾回收機制

1、技術背景

  GC的歷史比Java久遠,早在1960年Lisp這門語言中就使用了內存動態分配和垃圾回收技術html

2、那些內存須要回收?

  JVM的內存結構包括五大區域:程序計數器、虛擬機棧、本地方法棧、堆區、方法區。其中程序計數器、虛擬機棧、本地方法棧3個區域歲線程生滅,所以這幾個區域的內存分配和回收都具有肯定性,就不須要過多考慮回收的問題,由於方法借宿或者線程結束時,內存天然就跟着回收了。而Java堆和方法區不同,這部份內存的分配和回收是動態的,正是垃圾收集器所須要關注的。java

  垃圾收集器在對堆區和方法區進行回收前,首先要肯定這些區域的對象哪些能夠被回收,哪些暫時不能被回收,這就要用到判斷對象是否存活的算法!面試

2.1 引用計數算法

2.1.1 算法分析

  引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每一個對象實例都有一個引用計數。當一個對象被建立時,就將該對象實例分配給一個變量,該變量計數設置爲1.當任何其它變量被複製爲這個對象的引用時,計數加1(a=b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例能夠被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1。算法

2.1.2 優缺點

優勢:引用計數收集器能夠很快的執行,交織在程序運行中。對程序須要不被長時間打斷的實時環境比較有利服務器

缺點:沒法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0。多線程

public class ReferenceFindTest {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
          
        object1.object = object2;
        object2.object = object1;
          
        object1 = null;
        object2 = null;
    }
}

  這段代碼是用來驗證循環引用,最後兩句代碼將object1和object2賦值爲null,也就是說object1和object2指向的對象已經不可能再被訪問,可是因爲它們互相引用對方,致使它們的引用計數器都不爲0,那麼垃圾收集器就永遠不會回收它們併發

2.2 可達性分析算法

  可達性分析算法是從離散數學中的圖論引入的,程序把全部的引用關係看作一張圖,從一個節點GC Root開始,尋找對應的引用節點,找到這個節點之後,繼續尋找這個節點的引用節點,當全部引用節點尋找完畢以後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點,無用的節點將會被斷定爲可回收的對象。高併發

  在Java語言中,可做爲GC Roots的對象包括下面幾種:性能

  a)虛擬機棧中引用的對象(棧幀中的本地變量表);學習

  b)方法區中類靜態屬性引用的對象;

  c)方法區中常量引用的對象;

  d)本地方法棧中JNI(Native方法)引用的對象。

2.3 Java中的引用

  不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象的引用鏈是否可達,斷定對象是否存活都與「引用」有關。在Java語言中,將引用又分爲強引用、軟引用、弱引用、虛引用四種,這四種引用強度依次逐漸減弱。

  • 強引用

  在程序代碼中廣泛存在的,相似Object obj = new Object()這類引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

  • 軟引用

  用來描述一些還有用但並不是必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列入回收範圍之中進行第二次回收。若是此次回收後尚未足夠的內存,纔會拋出內存溢出異常。(在快要內存不足前的時候纔會進行回收)

  • 弱引用

  也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集以前。當垃圾收集器工做時,不管當時內存是否足夠,都會回收掉只被弱引用關聯的對象。  

  • 虛引用

  也叫幽靈引用或幻影引用,是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。它的做用是能在這個對象被收集時收到一個系統通知。

   經過對這四個概念的解釋,說明不管引用計數算法仍是可達性分析算法都是基於強引用而言的。

2.4 對象被回收前的最後一次掙扎

  即便在可達性分析算法中不可達的對象,也並不是是「非死不可」,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少須要經歷兩次標記過程。

  第一次標記:若是對象在進行可達性分析算法分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記;

  第二次標記:第一次標記後接着會進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。在finalize()方法中沒有從新與引用鏈創建關聯關係的,將會被第二次標記。

  第二次標記成功的對象將會真的被回收,若是對象在finalize()方法中從新與引用鏈創建了關聯,那麼將會逃離本次回收,繼續存活。

2.5 方法區如何判斷是否須要回收

  方法區存儲內容是否須要回收的判斷可就不同咯。方法區主要回收的內容有:廢棄常量和無用的類。對於廢棄常量也可經過引用的可達性來判斷,可是對於無用的類則須要同時知足下面3個條件:

  • 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
  • 加載該類的ClassLoader已經被回收;
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。(類在內存中是佔4個字節的)

3、經常使用的垃圾收集算法

3.1 標記-清除算法

  標記-清除算法此阿勇從根集合(GC Roots)進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收,以下圖所示。標記-清除算法不須要進行對象的移動,只需對不存活的對象進行處理,在存活對象比較多的狀況下極爲高效,可是因爲標記-清除算法直接回收不存活的對象,所以會形成內存碎片。

3.2 複製算法

  複製算法的提出是爲了克服句柄的開銷和解決內存碎片的問題。它開始時把堆分紅一個對象面和多個空閒面,程序從對象面爲對象分配空間,當對象滿了,基於copying算法的垃圾收集就從根集合(GC Roots)中掃描存活的對象,並將每一個活動對象複製到空閒面(使得活動對象所佔的內存之間沒有空閒洞),這樣空閒面變成了對象面,原來的對象面變成了空閒面,程序會在新的對象面中分配內存。

 

3.3 標記-整理算法

  標記-整理算法採用標記-清除算法同樣的方式進行對象的標記,但在清除時不一樣,在回收不存活的對象佔用的空間後,會將全部的存活對象往左端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,所以成本更高,可是卻解決了內存碎片的問題。具體流程以下圖:

 

3.4 分代收集算法

  分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區分爲老年代(Tenured Generation)和新生代(Young Generation),在堆區以外還有一個代就是永久代(Permanet Generation)。老年代的特色是每次垃圾收集時只有少許對象須要被回收,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。

3.4.1 年輕代(Young Generation)的回收算法

a)全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象;

b)新生代內存按照8:1:1的比例分爲一個eden區和亮哥survivor(survivor0,survivor1)區。一個Eden區,亮哥Survivor區(通常而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,而後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor區存活對象複製到另外一個survivor1區,而後清空eden和這個survivor0區,此時survivor0區是空的,而後將survivor0區和survivor1區交換,即保持survivor1區爲空,如此往復。

c)當survivor1區不足以存放eden和survivor0的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收。

d)新生代發生的GC也叫作Minor GC,Minor GC發生頻率比較高(不必定等Eden區滿了才觸發)

3.4.2 老年代(Old Generation)的回收算法

a)在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會放到老年代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。

b)內存比新生代也大不少(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率比較高

3.4.4 持久代(Permanent Generation)的回收算法

用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。持久代也稱方法區,具體參見上文2.5節。

4、常見的垃圾收集器

下圖是HotSpot虛擬機包含的全部收集器:

一、Serial收集器(複製算法)

新生代單線程收集器,優勢簡單高效。它在進行垃圾收集時,必須暫停其餘全部工做線程(用戶線程)。是JVM client模式下默認的新生代收集器。能夠經過 -XX:+UseSerialGC 來強制指定

二、Serial Old收集器(標記-整理算法)

老年代單線程收集器,Serial收集器的老年代版本。主要使用在Client模式下的虛擬機。對於Server模式下有兩個用途:一、在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用;2.做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

三、ParNew收集器(並行回收)

新生代收集器,Serial的多線程版本,除了使用多條線程進行垃圾收集外,其他行爲與Seria收集器同樣,在單CPU工做環境內絕對不會有比Serial收集器有更好的效果。隨着CPU數量的增長,它對於GC時系統資源的有效利用仍是頗有好處的,它默認開啓的收集線程數和CPU的數量相同,可使用 -XX:ParallelGCThreads 參數來限制線程數

四、Parallel Scavenge收集器(複製算法,並行回收)

也是一個新生代收集器,使用複製算法,且是並行多線程收集器。特色是它的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量縮短垃圾收集時用戶線程的停頓時間,二Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。吞吐量=程序運行時間/(程序運行時間 + 垃圾收集時間),虛擬機總共運行了100分鐘。垃圾收集花費1分鐘,吞吐量就是99%。它的吞吐量通常爲99%。Parallel Scavenge提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis 和直接設置吞吐量大小的 -XXGCTimeRatio 參數

收集器還有一個開關: -XX:+UseAdaptiveSizePolicy 值得關注。這個開關打開後,虛擬機會根據當前系統的運行狀況收集性能監控信息自動調整新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數。

自適應策略,只須要設置最大堆(-Xmx),利用最大停頓時間或者吞吐量給虛擬機設置一個優化目標。

此收集器適合後臺應用等對交互相應要求不高的場景,是server級別默認採用的GC方式,可用 -XX:+UseParallelGC 來強制指定,用 -XX:ParallelGCThreads=4 來指定線程數

五、Parallel Old收集器(標記-整理算法,並行)

是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。

六、CMS(Concurrent Mark Sweep)收集器(標記-清除算法,併發GC)

是一種以獲取最短回收停頓時間爲目標的收集器。適合應用在互聯網站或者B/S系統的服務器上,這類應用尤爲重視服務器的響應速度,但願系統停頓時間最短。高併發、低停頓,追求最短GC回收停頓時間,cpu佔用比較高,響應時間快,停頓時間短,多核cpu追求高響應時間的選擇。

  • Concurrent Mark Sweep 併發標記清除,併發低停頓
  • 標記-清除算法
  • 併發階段會下降吞吐量(由於停頓時間減小了,因而GC的頻率會變高)
  • 老年代收集器(新生代使用ParNew)
  • -XX:+UseConcMarkSweepGC   打開這收集器

注:這裏的併發指的是與用戶線程一塊兒執行。

整個收集過程分爲4個步驟:(着重實現了標記的過程)

① 初始標記(CMS initial mark)

根能夠直接關聯到的對象

速度快

② 併發標記(CMS concurrent mark)(和用戶線程一塊兒)

主要標記過程,標記所有對象

③ 從新標記(CMS remark)

因爲併發標記時,用戶線程依然運行,所以在正式清理前,再作修正

④ 併發清除(CMS)(和用戶線程一塊兒)

基於標記結果,直接清理對象

 

整個過程以下圖所示:

其中,初始標記和從新標記時,須要stop the world。

整個過程當中耗時最長的是併發標記和併發清除,這兩個過程均可以和用戶線程一塊兒工做。

CMS收集器的優勢:併發收集、低停頓

缺點:

1. CMS收集器對CPU資源很是敏感。在併發階段,雖然不會致使用戶線程停頓,可是會佔用CPU資源而致使引用程序變慢,總吞吐量降低。CMS默認啓動的回收線程數是:(CPU數量+3) / 4。虛擬機提供了一種稱爲「增量式併發收集器」的CMS收集器變種,能夠在併發標記、清理的時候讓GC線程、用戶線程交替運行,儘可能減小GC線程的獨佔資源的時間。
2. CMS收集器沒法處理浮動垃圾,可能出現「Concurrent Mode Failure「,失敗後而致使另外一次Full GC的產生。因爲CMS併發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,CMS沒法在本次收集中處理它們,只好留待下一次GC時將其清理掉。這一部分垃圾稱爲「浮動垃圾」。也是因爲在垃圾收集階段用戶線程還須要運行,即須要預留足夠的內存空間給用戶線程使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部份內存空間提供併發收集時的程序運做使用。在默認設置下,CMS收集器在老年代使用了68%的空間時就會被激活,也能夠經過參數-XX:CMSInitiatingOccupancyFraction的值來提供觸發百分比,以下降內存回收次數提升性能。要是CMS運行期間預留的內存沒法知足程序其餘線程須要,就會出現「Concurrent Mode Failure」失敗,這時候虛擬機將啓動後備預案:臨時啓用Serial Old收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。因此說參數-XX:CMSInitiatingOccupancyFraction設置的太高將會很容易致「Concurrent Mode Failure」失敗,性能反而下降。
3. 碎片化,最後一個缺點,CMS是基於「標記-清除」算法實現的收集器,使用「標記-清除」算法收集後,會產生大量碎片。空間碎片太多時,將會給對象分配帶來不少麻煩,好比說大對象,內存空間找不到連續的空間來分配不得不提早觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關參數,用於在Full GC以後增長一個碎片整理過程,還可經過-XX:CMSFullGCBeforeCompaction參數設置執行多少次不壓縮的Full GC以後,跟着來一次碎片整理過程。

既然標記清除算法會形成內存空間的碎片化,CMS收集器爲何使用標記清除算法而不是使用標記整理算法:

答案:

    CMS收集器更加關注停頓,它在作GC的時候是和用戶線程一塊兒工做的(併發執行),若是使用標記整理算法的話,那麼在清理的時候就會去移動可用對象的內存空間,那麼應用程序的線程就頗有可能找不到應用對象在哪裏。

七、G1收集器(標記-整理)

G1(Garbage First)收集器是JDK1.7提供的一個新收集器,G1收集器基於「標記-整理」算法實現,也就是說不會產生內存碎片。還有一個特色以前的收集器進行收集的範圍都是整個新生代或老年代,而G1將整個Java堆(包括新生代,老年代)。

G1收集器的特色:

**並行與併發:**G1利用多CPU、多核環境下的硬件優點,縮小stop-the-world的時間。
**分代收集:**G1不須要其餘收集器配合就能夠獨立管理整個GC堆,但它可以採用不一樣的方式來處理。
空間整合:總體上是「標記-整理」,局部上是基於「複製」的算法來實現的
可預測的停頓:下降停頓時間,G1創建了可預測的停頓時間模型,能讓使用者明確的指定在一個長度M毫秒內的時間片斷,消耗在垃圾收集的時間不得超過N毫秒,這已經適實時java(RTSJ)的垃圾收集器的特徵了
G1收集的步驟:

  初始標記
  併發標記
  最終標記
  篩選回收

5、GC是何時出發的(面試最多見的問題之一)

因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。GC有兩種類型:Scavenge GC和Full GC。

5.1 Scavenge GC

通常狀況下,當新對象生成,而且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,而且把尚且存活的對象移動到Survivor區。而後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。由於大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,因此Eden區的GC會頻繁進行。於是,通常在這裏須要使用速度快、效率高的算法,使Eden去能儘快空閒出來。

5.2 Full GC

對整個堆進行整理,包括Young、Tenured和Perm。Full GC由於須要對整個堆進行回收,因此比Scavenge GC要慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於Full GC的調節。有以下緣由可能致使Full GC:

a) 年老代(Tenured)被寫滿;

b) 持久代(Perm)被寫滿;

c) System.gc()被顯示調用;

d) 上一次GC以後Heap的各域分配策略動態變化;

 

原文連接:

  https://www.cnblogs.com/1024Community/p/honery.html

參考連接:

  https://blog.csdn.net/qq_27035123/article/details/72857739

  http://www.javashuo.com/article/p-hdtqeszz-gn.html

相關文章
相關標籤/搜索