摘要:就是說引用計數法很難解決對象之間的相互引用問題也無法通知回收器去及時回收它。因為存在這種缺點,所以現在的虛擬機基本上并不是通過引用計數法來判斷對象是否存活。
1、為什么要進行垃圾回收?
每當在我們寫代碼的時候,不管是new一個對象,還是引用,還是填充數據到數組,都是要占用空間,那么如果不及時回收就會對系統的運行產生影響。java和c 一個很大的區別就在于,java的垃圾回收主要是jvm去做,而c語言是自己去控制。雖然JAVA可以手動的調用方法 system.gc 去手動控制垃圾回收,但據說達不到立馬回收的效果。c 語言則是要自己去申請一塊內存空間malloc ,使用完成還需要手動去釋放掉,如果沒有及時釋放,或者申請出現內存過大等,會造成內存溢出等異常,不過功底深厚的大牛都會做的比較牛逼,很好的去控制。
2、如何判斷對象生死?在內存回收的過程中,首先要確定一點,該對象是否應該被回收,哪些還"存活",哪些已"死亡"。
引用計數法
當一個對象在代碼中被引用,那么就會在這個對象的引用計數值 + 1,當引用失效的時候,計數值則相應 - 1。但是如果兩個對象存在相互引用的情況,如下:
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...
注:并不是不可達的對象就必須 "死",他們還是處于"緩刑", 真正要宣告一個對象死亡,需要經過兩次標記的過程:經過可達性分析后對象沒有和GC Roots 連接的引用鏈,那么需要被標記一次然后還需要經過篩選(篩選條件:判斷該對象是否有必要執行finalize()方法),如果對象已經調用了或者沒有覆蓋finalize方法(finalize() 方法只會被執行一次!),那么 虛擬機判定該對象是 "沒有必要執行該方法"。
如果該對象有必要執行finalize方法,那么對象會被放置在一個叫做F-Queue 的隊列之中,之后會由虛擬機自動建立,由低優先級的Finalize 方法去執行。(執行時只去觸發對象的finalize()方法,但是并不等待他運行結束,防止有的對象finalize()進行緩慢,或者死循環,會導致隊列持續等待,進而內存回收系統崩潰。)稍后GC 會對F-Queue 隊列中的對象進行第二次標記,當finalize 方法執行后成功將對象連接到引用鏈上任何一個對象,那么這個對象就被拯救成功了,不然則go die!
什么是引用?Java中定義:如果reference類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱是這塊內存的一個引用。
強引用:類似于A a = new A(),只要引用在,就永遠不會回收被引用的對象。
軟引用:描述有用但并非必須的獨享。在系統將要發生內存溢出的時候,會將這部分對象回收。
弱引用:非必需對象引用,能生存到下次發生GC之前。
虛引用:一點用都沒。只有當對象被垃圾回收器回收的時候會收到一個系統通知。
方法區回收在我看來方法區的內存,回收起來并沒有新生代那么明顯。方法區大多存有類的描述信息,靜態變量,常量,方法等信息,這些大多是系統常用的,很少去回收,回收效率微乎其微。然而永久帶的方法回收主要分成兩部分,一種是常量的回收,另一種是類的回收。
常量的回收
相當于這個常量已經被廢棄掉了。例如:方法區的常量池中有一個字符串常量 "java", 當系統中沒有一個String對象指向這個常量的值得時候,那么這個常量在發生GC的時候將會被回收。
類的回收
類的回收
相對于常量的回收會麻煩多,需要滿足下面三個條件才會被回收:
1、類中所有的對象都被回收,就是堆中不存在該類的任何的實例;
2、加載該類的classloader被回收;
3、該類對應的java.lang.Class對象沒有在任何地方引用,或是通過反射機制訪問不到該類。
注: 在大量使用反射,動態代理,cGLib等ByteCode框架、動態生成Jsp等頻繁定義classLoader的場景都需要虛擬機具備類的卸載功能,防止永久帶不會溢出。
3、垃圾回收算法標記-清除算法
標記-清除 算法是最基礎的算法,為什么呢?因為后面的要講的算法很多是從這個基本的算法改變其不足演變而來。
標記-清除(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。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/66858.html
摘要:垃圾回收算法與垃圾回收器綜述我們常說的垃圾回收算法可以分為兩部分對象的查找算法與真正的回收方法。串行垃圾回收器一次只使用一個線程進行垃圾回收并行垃圾回收器一次將開啟多個線程同時進行垃圾回收。 垃圾回收算法與 JVM 垃圾回收器綜述歸納于筆者的 JVM 內部原理與性能調優系列文章,文中涉及的引用資料參考 Java 學習與實踐資料索引、JVM 資料索引。 showImg(https://s...
垃圾回收(GC)是JVM的一大殺器,它使程序員可以更高效地專注于程序的開發設計,而不用過多地考慮對象的創建銷毀等操作。但是這并不是說程序員不需要了解GC。GC只是Java編程中一項自動化工具,任何一個工具都有它適用的范圍,當超出它的范圍的時候,可能它將不是那么自動,而是需要人工去了解與適應地適用。 擁有一定工作年限的程序員,在工作期間肯定會經常碰到像內存溢出、內存泄露、高并發的場景。這時候在應對這...
摘要:這個算法看似不錯而且簡單,不過存在這一個致命傷當兩個對象互相引用的時候,就永遠不會被回收于是引用計數算法就永遠回收不了這兩個對象,下面介紹另一種算法。 前言 ? 如果要問Java與其他編程語言最大的不同是什么,我第一個想到的一定就是Java所運行的JVM所自帶的自動垃圾回收機制,以下是我學習JVM垃圾回收機制整理的筆記,希望能對讀者有一些幫助。 哪些內存需要回收?what? ? ...
摘要:在這種消耗很高的狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。 Java 垃圾回收(GC) 泛讀 文章地址: https://segmentfault.com/a/1190000008922319 0. 序言 帶著問題去看待 垃圾回收(GC) 會比較好,一般來說主要的...
閱讀 1639·2021-09-02 15:11
閱讀 1976·2019-08-30 14:04
閱讀 2563·2019-08-27 10:52
閱讀 1583·2019-08-26 11:52
閱讀 1203·2019-08-23 15:26
閱讀 2623·2019-08-23 15:09
閱讀 2606·2019-08-23 12:07
閱讀 2234·2019-08-22 18:41