Jvm垃圾收集器和垃圾回收算法

概述:java

  目前內存的動態分配和內存的回收技術已經至關成熟,一切看起來都已經進入了「自動化」時代,爲何還要去了解GC和內存分配呢?緣由很簡單:當須要排查各類內存泄漏、內存溢出問題時,當垃圾收集器成爲系統達到更高併發量的瓶頸時,咱們就須要對這些「自動化」的技術實施必要的監控和調節。算法

  以前的博客講到了Java虛擬機運行時內存的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅,棧中的棧幀隨方法的進入和退出執行着入棧和出棧操做,所以方法結束或者線程結束時,內存就天然跟隨着回收了。而Java堆和方法區組不一樣,咱們在運行時才知道會建立哪些對象,這部分的內存和回收都是動態的,垃圾回收器所關注的也是這一部份內存。後文所講的內存分配和回收也是僅指這一部份內存。併發

1、判斷對象「死活」

  1.引用計數法

  不少教科書或者開發人員,對於回答「如何判斷對象死活?」這個問題的答案大可能是引用計數法。實現起來是這樣的:給對象添加一個引用計數器,每當一個地方引用它時就+1,當引用失效時就-1,任什麼時候刻計數器爲0的對象就是不願能再被引用的。框架

    客觀的說,引用計數法實現簡單,斷定效率也很高,可是至少主流的Java虛擬機都沒有采用引用計數法來管理內存,最主要的緣由是它很難解決對象之間互相循環引用的問題。jvm

  2.可達性分析算法

  可達性算法(Reachability Anlysis)是目前Java、C#的主流實現中來斷定對象是否存活的。主要思路是:經過一些類成爲「GC Roots」的對象作爲起點,從這些結點開始往下搜索,搜索所走過的路稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈時,則證實這個對象是不可用的。ide

    下圖所示:高併發

  

     Object1-Object4均有引用鏈和GC Roots相鏈接,而Object5-Oject7沒有引用鏈相連,因此他們被斷定爲能夠回收的對象this

  在Java中,能夠作爲GC Roots結點的有一下四種spa

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

3.再談引用

  在JDK1.2以前,Java中的引用定義爲:若是reference類型的數據存儲的是數值表明的是另外一塊內存的起始地址,就稱爲這塊內存表明着一個引用。在JDK1.2以後,Java對引用概念進行了擴充,分爲下面四種:線程

  • 強引用(Strong Reference):代碼中廣泛存在的,相似Object obj=new Object(),這類的引用,只要強引用還存在,GC就永遠不會回收掉被引用的對象。
  • 軟引用(Soft Reference):用來描述一些還有用但並不是必需的對象。對於軟引用關聯的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收,若是此次回收以後尚未足夠的內存,纔會拋出內存溢出異常。在JDK1.2以後,提供了SoftReference類來實現軟引用。
  • 弱引用(Weak Reference):也是用來描述非必需的對象,但強度比軟引用要弱,被弱引用關聯的對象只能生存到下一次垃圾回收器以前,當垃圾回收器工做時,必定會回收只被弱引用關聯的對象。在JDK1.2以後,提供了WeakReference類來實現弱引用。
  • 虛引用(Phantom Reference):也稱爲幽靈引用或者幻影引用,是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來得到一個對象實例。爲一個對象設置虛引用關聯的惟一目的:在這個對象被垃圾回收器回收時收到一個系統通知。

4.生存仍是死亡

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

  • 若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那麼它將被第一次標記而且執行一次篩選。篩選的條件是:這個對象是否有必要執行finalize()方法(若對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過(虛擬機只能調用一次),則視爲「沒有必要執行」)。
  • 若被斷定爲有必要執行finalize()方法,那麼該對象會被放置在一個叫作F-Queue的隊列中,並在稍後有一個虛擬機自動創建的、低優先級的Finalizer線程去執行它(即虛擬機觸發finalize()方法,但不承諾會保證等待它運行結束,由於若是一個對象的finalize()方法執行緩慢,或者發生了死循環,將極可能會致使F-Queue隊列中的其餘對象永遠處於等待狀態,甚至整個內存回收系統崩潰)。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次標記。若是對象要在這裏拯救本身,就要從新與引用鏈上的任意一個對象創建關聯便可。譬如,把本身複製給某個類變量或者對象的成員變量上,那麼在第二次標記它時將被移除F-Queue隊列。若是對象尚未逃脫,則被真正的回收了。

下面代碼顯示一次對象的自我拯救:

 1 //代碼演示了兩點
 2 //1.對象能夠實現自救
 3 //2.這種自救只能一次,由於finalize方法只會被執行一次
 4 
 5 public class jvmtest1 {
 6     public static jvmtest1 obj = null;
 7 
 8     public void isAlive() {
 9         System.out.println("I am alive");
10     }
11 
12     @Override
13     protected void finalize() throws Throwable {
14         super.finalize();
15         System.out.println("finalize method executed!");
16         obj = this;
17     }
18 
19     public static void main(String[] args) throws Exception {
20         obj = new jvmtest1();
21 
22         obj = null;
23         System.gc();
24         // 由於finalize方法優先級很低,因此暫停0.5s執行
25         Thread.sleep(500);
26         if (obj != null) {
27             obj.isAlive();
28         } else {
29             System.out.println("I am dead");
30         }
31 
32         // 與上面的代碼徹底相同,可是自救失敗了
33         obj = null;
34         System.gc();
35         // 由於finalize方法優先級很低,因此暫停0.5s執行
36         Thread.sleep(500);
37         if (obj != null) {
38             obj.isAlive();
39         } else {
40             System.out.println("I am dead");
41         }
42     }
43 }

    程序輸出爲:

                                     

   能夠看出obj的finalize()方法,確實被GC觸發過,並且成功自救了一次。要注意的是下半部分的代碼和上半部分的代碼相同,可是自救失敗,這是由於一個對象的finalize()方法只能被系統自動調用一次,若是對象面臨第二次回收,其finalize()方法不會被調用,所以第二次自救失敗。

  5.回收方法區

    方法區的垃圾收集「性價比」很低,在堆中,尤爲是新生代中,常規應用進行一次垃圾收集通常能夠回收70%-95%的空間,而方法區(HatSport虛擬機中的永久代)的回收效率遠低於    此。

  永久代中的垃圾收集主要爲兩種:廢棄常量和無用的類

  • 回收廢棄常量與回收Java堆中的對象很是相似。以常量池中字面量的回收爲例:假如「abc」已經進入了常量池,可是沒有任何String對象引用這個「abc」常量,也沒有其餘地方引用這個字面量,若是這時發生內存回收,並且有必要的話,這個「abc」常量就會被清出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似
  • 判斷一個類是不是無用類條件要複雜不少。類須要同時知足下面三個條件纔是無用類:

    (1)該類的全部實例都已經被回收,也就是堆中不存在該類的任何實例

    (2)加載該類的ClassLoader已經被回收

    (3)該類對應的java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

  對於知足上面三個條件的類,也是能夠回收,不是必然回收。在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。

2、垃圾回收算法

  1.標記-清除算法

    標記-清除算法(Mark-Sweep)是最基礎的收集算法。首先標記出所須要回收的對象,在標記完成後統一回收全部被標記的對象。

    不足之處主要有兩點:

  • 效率問題,標記和清除兩個過程的效率都不高。
  • 空間問題,清除以後會產生大量的不連續的內存碎片,內存碎片太多可能會致使要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發下一次垃圾收集動做。

過程以下:

2.複製算法

  爲了解決效率問題,複製算法(Copying)出現了,他將內存分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就把存活的對象複製到另外一塊上,而後把這一塊空間所有清理掉。這樣是的每次是對整個半區就行內存回收,內存分配時也不用考慮內存碎片的複雜狀況,只須要移動指針,按順序分配內存便可。

  只是這種方法的代價是將內存分爲以前的一半,未免過高了。

過程以下:

  如今的商業虛擬機都是採用這種方法來回收新生代,IBM公司的專門研究代表,新生代中98%的對象是「朝生夕死」的,因此並不須要1:1來劃份內存空間。另外一種方法是:將內存塊分爲Eden空間(80%)和兩塊Survivor空間(10%),每次使用Eden空間和一塊Survivor空間,回收時將存活的對象複製到另外一塊Survivor空間上,這樣每次只有10%的空間被「浪費」。當Survivor空間不夠用時,須要依賴其餘內存(老年代)來進行分配擔保(同過度配擔保機制進入老年代)。

3.標記-整理算法

  當複製收集算法在對象存活率較高時就要進行較多的複製動做,效率會變慢。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此老年代通常不能選用這種方法。標記-整理算法(Mark-Compact)就被提出了。

  標記過程和「標記-清除」算法同樣,可是後續步驟不是直接對可回收部分進行清理,而是把存活對象都向一端移動,而後直接清理掉端之外的內存。

過程以下:

 

   4.分代收集算法

   當前商業虛擬機都採用分代收集算法(Generational Collection),通常是把堆分爲新生代和老年代,根據各個年代的不一樣特色採用最適合的算法。新生代通常採用複製算法,只須要付出少許存活對象的複製成本便可;老年代由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或者「標記-整理」算法來進行回收。

相關文章
相關標籤/搜索