JVM 對象引用標記 與 內存回收算法

一、爲何要進行垃圾回收?


每當在咱們寫代碼的時候,無論是new一個對象,仍是引用,仍是填充數據到數組,都是要佔用空間,那麼若是不及時回收就會對系統的運行產生影響。java和c 一個很大的區別就在於,java的垃圾回收主要是jvm去作,而c語言是本身去控制。雖然JAVA能夠手動的調用方法 system.gc 去手動控制垃圾回收,但聽說達不到立馬回收的效果。c 語言則是要本身去申請一塊內存空間malloc ,使用完成還須要手動去釋放掉,若是沒有及時釋放,或者申請出現內存過大等,會形成內存溢出等異常,不過功底深厚的大牛都會作的比較牛逼,很好的去控制。java

二、如何判斷對象生死?

在內存回收的過程當中,首先要肯定一點,該對象是否應該被回收,哪些還"存活",哪些已"死亡"。算法

  • 引用計數法

當一個對象在代碼中被引用,那麼就會在這個對象的引用計數值 + 1,當引用失效的時候,計數值則相應 - 1。可是若是兩個對象存在相互引用的狀況,以下:segmentfault

A a = new A(); 
    B b = new B();
    a.instance = b;
    b.instance = a;

虛擬機就沒法去判斷這兩個對象是否須要被回收,由於彼此的引用值都不是 0。就是說引用計數法很難解決對象之間的相互引用問題, 也沒法通知GC回收器去及時回收它。由於存在這種缺點,因此如今的虛擬機基本上並非經過引用計數法來判斷對象是否存活。那是經過什麼方法? 請看官往下看。數組

  • 可達性分析算法

如今主流的商用語言的視線中都是經過可達性分析來判斷對象是否存活,好比JAVA,C#等。這種方法基本思想 ——以 GC Roots的對象做爲起點向下搜索,搜索走過的路徑被稱爲"引用鏈",當一個對象沒有任何引用鏈相連,那麼這個對象就是不可用的。以下圖所示:安全

圖片描述

gc roots 是什麼? 是知足下面任意條件的某個對象。框架

  • 虛擬機棧中reference對象;
  • 方法區靜態屬性引用對象;
  • 方法區常量引用對象;
  • 本地方法棧 所謂的native方法 引用的對象。

Hotspot中的native方法引用Java對象用的是經過句柄(handle)來引用。HotSpot的JNI handle是放在若干不一樣的區域裏的,但不會放在GC堆中。傳遞參數用的handle直接在棧上;local handle放在每一個Java線程中的JNIHandleBlock裏;global handle放在VM全局的JNIHandleBlock裏。
關於什麼是native方法請以下連接:https://segmentfault.com/n/13...jvm

注:並非不可達的對象就必須 "死",他們仍是處於"緩刑", 真正要宣告一個對象死亡,須要通過兩次標記的過程:通過可達性分析後對象沒有和GC Roots 鏈接的引用鏈,那麼須要被標記一次而後還須要通過篩選(篩選條件:判斷該對象是否有必要執行finalize()方法),若是對象已經調用了或者沒有覆蓋finalize方法(finalize() 方法只會被執行一次!),那麼 虛擬機斷定該對象是 "沒有必要執行該方法"。spa

若是該對象有必要執行finalize方法,那麼對象會被放置在一個叫作F-Queue 的隊列之中,以後會由虛擬機自動創建,由低優先級的Finalize 方法去執行。(執行時只去觸發對象的finalize()方法,可是並不等待他運行結束,防止有的對象finalize()進行緩慢,或者死循環,會致使隊列持續等待,進而內存回收系統崩潰。)稍後GC 會對F-Queue 隊列中的對象進行第二次標記,當finalize 方法執行後成功將對象鏈接到引用鏈上任何一個對象,那麼這個對象就被拯救成功了,否則則go die!線程

什麼是引用?

Java中定義:若是reference類型的數據中存儲的數值表明的是另外一塊內存的起始地址,就稱是這塊內存的一個引用。3d

  • 強引用:相似於A a = new A(),只要引用在,就永遠不會回收被引用的對象。
  • 軟引用:描述有用但並不是必須的獨享。在系統將要發生內存溢出的時候,會將這部分對象回收。
  • 弱引用:非必需對象引用,能生存到下次發生GC以前。
  • 虛引用:一點用都沒。只有當對象被垃圾回收器回收的時候會收到一個系統通知。

方法區回收

在我看來方法區的內存,回收起來並無新生代那麼明顯。方法區大多存有類的描述信息,靜態變量,常量,方法等信息,這些大可能是系統經常使用的,不多去回收,回收效率微乎其微。然而永久帶的方法回收主要分紅兩部分,一種是常量的回收,另外一種是類的回收。

  • 常量的回收

至關於這個常量已經被廢棄掉了。例如:方法區的常量池中有一個字符串常量 "java", 當系統中沒有一個String對象指向這個常量的值得時候,那麼這個常量在發生GC的時候將會被回收。
類的回收

  • 類的回收

相對於常量的回收會麻煩多,須要知足下面三個條件纔會被回收:
一、類中全部的對象都被回收,就是堆中不存在該類的任何的實例;
二、加載該類的classloader被回收;
三、該類對應的java.lang.Class對象沒有在任何地方引用,或是經過反射機制訪問不到該類。

注: 在大量使用反射,動態代理,cGLib等ByteCode框架、動態生成Jsp等頻繁定義classLoader的場景都須要虛擬機具有類的卸載功能,防止永久帶不會溢出。

三、垃圾回收算法

  • 標記-清除算法

    標記-清除 算法是最基礎的算法,爲何呢?由於後面的要講的算法不少是從這個基本的算法改變其不足演變而來。

標記-清除(Mark-Sweep) 算法正如其名字所說由兩個部分來完成。首先,要對須要回收的對象進行標記,如何標記上面已經提過。而後,要對這些被標記的對象進行收集。

標記:標記的過程其實就是,遍歷全部的GC Roots,而後將全部GC Roots可達的對象標記爲存活的對象。
清除:清除的過程將遍歷堆中全部的對象,將沒有標記的對象所有清除掉。

以下圖所示:

圖片描述

感受清理完成以後,內存零零散散,故該算法有如下兩個缺點:
缺點一:被標記的對象在內存中分佈很零散,回收以後可用內存很零碎。若是當一個進程須要申請一塊連續的較大內存時,沒法找到足夠的連續內存,不得不提早觸發一次垃圾回收的動做。
缺點二標記和清除的過程效率不高,並且在進行GC的時候,須要中止應用程序,這會致使用戶體驗很是差,尤爲對於交互式的應用程序來講簡直是沒法接受。

複製算法

複製算法是對標記-清除算法在回收後出現不少內存碎片的一種改進,並且效率也有所提高。

複製算法(copying)將可用的內存容量劃分紅大小相等兩塊,每次只使用其中一塊,當一塊內存用完了,將還存活的對象複製到另外一塊內存中,而後將以前使用過的內存空間所有清理掉。當有效內存空間耗盡時,JVM將暫停程序運行,開啓複製算法GC線程 ,它會將活動區間內的存活對象,所有複製到空閒區間,且嚴格按照內存地址依次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址。 清空以前的內存塊,減小了大量不連續的內存碎片的產生,實現簡單且運行高效。

以下圖所示:
圖片描述

細心的讀者會發現,這種算法有個很大的缺點——將可以使用的內存縮小成原來的一半,代價太大了!因此如今的虛擬機廠商都採用複製算法來回收新生代。研究代表 新生代對象98%都是 「朝生夕死」 因此不須要按照1:1劃份內存空間,而是將內存分爲一塊較大的Eden 空間和兩塊較小的Survivor 空間。每次只是用 Eden 和 其中一塊 Survivor區域,另外一塊Survivor 則用來看成保留區。 那麼這樣一來,每次進行回收的時候只須要將Eden 和 Survivor生還的對象複製到另外一塊 Survivor空間,而後清理。HotShot虛擬機 新生代劃分比例默認 Eden:Survivor = 8:1。然而這樣分配內存,有個問題。看成保留區的Survivor的內存大小不夠承載 使用中的Eden和一塊Survivor區域的存活對象怎麼辦?此時須要依賴其餘內存(老年代)進行分配擔保

分配擔保(Handle Promotion)——若是另外一塊Survivor空間沒有足夠的空間存放上一次新生代收集下來的存活對象時,這些對象直接經過分配擔保機制進入老年代。

圖片描述

發生Minor GC(只回收Eden和Survivor區)前,虛擬機檢查老年代最大可用連續空間是否大於新生代全部對象總空間。若是大於,那麼Minor GC確保是安全的。若是不大於,則須要查看虛擬機HandlePromotionFailure參數設置,是否容許擔保失敗。若容許(true),會繼續檢查老年代最大連續可用空間是是否大於歷次晉升到老年代的對象平均大小。若是大於,會嘗試一次 Minor GC,儘管是有風險。(由於僅僅是歷次晉升到老年代對象平均大小與老生代最大連續空間比較,若是內存小沒法容納,此時進行Minor GC 會清理本來存活的對象因此是冒險的,進而須要進行Full GC)若是小於或者Handle Promotion Failure不容許冒險,那麼要進行一次Full GC。

在jdk1.6 update24之後handle promotion failure 參數已經不會影響到分配擔保的斷定,具體代碼以下:

bool TenuredGeneration::promotion_attempt_is_safe(size_t  
max_promotion_in_bytes) const   
{  
   // 老年代最大可用的連續空間  
   size_t available = max_contiguous_available();    
   // 每次晉升到老年代的平均大小  
   size_t av_promo  = (size_t)gc_stats()->avg_promoted()->padded_average();  
   // 老年代可用空間是否大於平均晉升大小,或者老年代可用空間是否大於當此GC時新生代全部對象容量  
   bool   res = (available >= av_promo) || (available >=max_promotion_in_bytes);  
   return res;  
}

若是老年代連續空閒空間大於歷屆晉升到老年代的對象的平均空間能夠直接minor GC 不然 Full GC。

標記-整理算法

複製算法是須要將對象從從內存一個區域複製到另外一個區域,當發現對象存活率很高的狀況下,效率很低。並且在老生代的回收中,大多不採用複製算法,沒有額外的空間進行分配擔保。

標記-整理算法(Mark-Compact),過程和Mark-Sweep 方法過程同樣,也須要對對象進行標記,不事後續步驟不是直接對可回收對象進行清理,算法分紅兩個部分。  

標記:遍歷GC Roots,而後將存活的對象標記。
整理:移動全部存活的對象,且按照內存地址次序依次排列,而後將末端內存地址之後的內存所有回收。

圖片描述

標記-整理算法不只能夠彌補標記-清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價。不過標記-整理算法,效率是惟一缺點。它須要對存活對象進行標記,而後要整理存活對象內存地址,相對於複製算法效率較低

分代收集算法

分代收集算法,當前商用的虛擬機的垃圾收集大都採用這種算法。也不算是算法,只是把java 堆分紅 新生代,老生代。分代以後,不一樣區域可使用不一樣的收集算法。好比:

  • 新生代 每次垃圾回收都會有大批對象死去,只有少許存活,那就採用複製-算法
  • 老生代 對象存活率高,也沒額外的空間對它進行分配擔保,使用標記-整理/清除算法會更好來回收。不一樣場景使用不一樣的算法更加有利於總體效率的提高。

JVM 在進行GC 時,並不是每次都對上面三個內存區域一塊兒回收的,大部分時候回收的都是指新生代。所以GC按照回收的區域又分了兩種類型,一種是普通GC(minor GC),一種是全局GC(major GC or Full GC),它們所針對的區域以下。
普通GC(minor GC):只針對新生代區域的GC。
全局GC(major GC or Full GC):針對年老代的GC,偶爾伴隨對新生代的GC以及對永久代的GC。

注:因爲年老代與永久代相對來講GC效果很差,並且兩者的內存使用增加速度也慢,所以通常狀況下,須要通過好幾回普通GC,纔會觸發一次全局GC。

相關文章
相關標籤/搜索