常見的垃圾回收算法詳解

什麼是垃圾

➢ 垃圾是指在運行程序中沒有任何指針指向的對象,這個對象就是需要被回收的垃圾。
➢ 外文: An object is considered garbage when it can no longer be reached from any pointer in the running progr am.

如果不及時對內存中的垃圾進行清理,那麼,這些垃圾對象所佔的內存空間會一直保留到應用程序結束,被保留的空間無法被其他對象使用。甚至可能導致內存溢出。隨着應用程序所應付的業務越來越龐大、複雜,用戶越來越多,沒有GC就不能保證應用程序的正常進行。而經常造成STW的GC又跟不上實際的需求,所以纔會不斷地嘗試對GC進行優化。

垃圾標記階段算法

在堆裏存放着幾乎所有的Java對象實例,在GC執行垃圾回收之前,**首先需要區分出內存中哪些是存活對象,哪些是已經死亡的對象。**只有被標記爲己經死亡的對象,GC纔會在執行垃圾回收時,釋放掉其所佔用的內存空間,因此這個過程我們可以稱爲垃圾標記階段
那麼在JVM中究竟是如何標記一個死亡對象呢?簡單來說,當一個對象已經不再被任何的存活對象繼續引用時,就可以宣判爲已經死亡。判斷對象存活一般有兩種方式:引用計數算法和可達性分析算法

引用計數算法

概念

引用計數算法(Reference Counting) 比較簡單,對每個對象保存一個整型的引用計數器屬性。用於記錄對象被引用的情況。|
對於一個對象A,只要有任何一一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的引用計數器的值爲0,即表示對象A不可能再被使用,可進行回收。.

優缺點

優點:實現簡單,垃圾對象便於辨識;判定效率高,回收沒有延遲性。
缺點:
➢ 它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷,其實就是一個變量,所以這個開銷也不算多。
➢ 每次賦值都需要更新計數器,伴隨着加法和減法操作,這增加了時間開銷
➢ 引用計數器有一個嚴重的問題,即無法處理循環引用的情況。這是一條致命缺陷,導致在Java的垃圾回收器中沒有使用這類算法
在這裏插入圖片描述
引用計數算法,雖然不能解決循環引用問題,但是優點也確實很明顯,是很多語言的資源回收選擇,例如因人工智能而更加火熱的Python,它更是同時支持引用計數和垃圾收集機制。
具體哪種最優是要看場景的,業界有大規模實踐中僅保留引用計數機制,以提高吞吐量的嘗試。
Java並沒有選擇引用計數,是因爲其存在一個基本的難題,也就是很難處理循環引用關係。那Python如何解決循環引用?
➢ 手動解除:很好理解,就是在合適的時機,解除引用關係。
➢ 使用弱引用weakref,weakref是 Python提供的標準庫,旨在解決循環引用。

可達性分析算法

概念

可達性分析(也稱根搜索算法追蹤性垃圾收集)
相對於引用計數算法而言,可達性分析算法不僅同樣具備實現簡單和執行高效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生
相較於引用計數算法,這裏的可達性分析就是Java、C#選擇的。這種類型的垃圾收集通常也叫作追蹤性垃圾收集(Tracing Garbage Collection)。

基本思路

➢ 可達性分析算法是以根對象集合(GC Roots :就是一組必須活躍的引用)爲起始點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達
➢ 使用可達性分析算法後,內存中的存活對象都會被根對象集合直接或間接連接着,搜索所走過的路徑稱爲引用鏈(Reference Chain)
➢ 如果目標對象沒有任何引用鏈相連,則是不可達的,就意味着該對象己經死亡,可以標記爲垃圾對象。
➢ 在可達性分析算法中,只有能夠被根對象集合直接或者間接連接的對象纔是存活對象
在這裏插入圖片描述

常見GC roots元素

在Java語言中, GC Roots包括以下幾類元素:(從上到下最爲常見,最後一個比較高端)

  • 虛擬機棧中引用的對象,比如:各個線程被調用的方法中使用到的參數、局部變量等。
  • 本地方法棧內JNI (通常說的本地方法)引用的對象
  • 方法區中類靜態屬性引用的對象,比如: Java類的引用類型靜態變量
  • 方法區中常量引用的對象,比如:字符串常量池(string Table)裏的引用
  • 所有被同步鎖synchroni zed持有的對象
  • Java虛擬機內部的引用。基本數據類型對應的Class對象,一些常駐的異常對象(如:NullPointerException、OutOfMemoryError),系統類加載器。
  • 反映java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。
  • 除了這些固定的GCRoots集合以外,根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,還可以有其他對象“臨時性”地加入,共同構成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)。
    ➢ 例如:如果只針對Java堆中的某一塊塊區域進行垃圾回收(比如:典型的只針對新生代),必須考慮到內存區域是虛擬機自己的實現細節,更不是孤立封閉的,這個區域的對象完全有可能被其他區域的對象所引用,這時候就需要一併將關聯的區域對象也加入GC Roots集合中去考慮,才能保證可達性分析的準確性。

GCRoots集合小技巧:由於Root採用棧方式存放變量和指針,所以如果一個指針,它保存了堆內存裏面的對象,但是自己又不存放在堆內存裏面,那它就是一個Root。

注意:

如果要使用可達性分析算法來判斷內存是否可回收,那麼分析工作必須在一個能保障一致性的快照中進行。這點不滿足的話分析結果的準確性就無法保證。也就是說在分析期間,整個執行系統就像被凍結在某個時間點上,不能在分析過程中對象的引用關係還在不停變化。這點也是導致GC進行時必須"Stop The World"的一個重要原因。即使是號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必須要停頓的。

對象的finalization機制

  • Java語言提供了對象終止(finalization)機制來允許開發人員提供對象被銷燬之前的自定義處理邏輯
  • 當垃圾回收器發現沒有引用指向一個對象, 即:垃圾回收此對象之前,總會先調用這個對象的finalize()方法。finalize()方法允許在子類中被重寫,用於在對象被回收時進行資源釋放。通常在這個方法中進行一些資源釋放和清理的工作,比如關閉文件、套接字和數據庫連接等。
  • 永遠不要主動調用某個對象的finalize()方法,應該交給垃圾回收機制調用。理由包括下面三點:
    ➢ 在finalize() 時可能會導致對象復活
    ➢ finalize() 方法的執行時間是沒有保障的,它完全由GC線程決定, 極端情況下若不發生GC,則finalize()方法將沒有執行機會。
    ➢ 一個糟糕的finalize ()會嚴重影響Gc的性能。
  • 從功能上來說,finalize() 方法與C++中的析構函數比較相似,但是Java採用的是基於垃圾回收器的自動內存管理機制,所以finalize()方法在本質上不同於C+ +中的析構函數。
  • 由於finalize ()方法的存在,虛擬機中的對象一般處於三種可能的狀態

如果從所有的根節點都無法訪問到某個對象,說明對象已經不再使用了。一般來說,此對象需要被回收。但事實上,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段。一個無法觸及的對象有可能在某一個條件下“復活”自己,如果這樣,那麼對它的回收就是不合理的,爲此,定義虛擬機中的對象可能的3種狀態。如下:

➢ 可觸及的:從根節點開始,可以到達這個對象。
➢ 可復活的:對象的所有引用都被釋放,但是對象有可能在finalize()中復活
➢ 不可觸及的:對象的finalize()被調用,並且沒有復活,那麼就會進入不可觸及狀態。不可觸及的對 象不可能被複活,因爲finalize() 只會被調用一次

以上3種狀態中,是由於finalize ()方法的存在,進行的區分。只有在對象不可觸及時纔可以被回收。

判定一個對象objA是否可回收,至少要經歷兩次標記過程:

  1. 如果對象objA到GC Roots沒有引用鏈,則進行第一次標記。
  2. 進行篩選,判斷此對象是否有必要執行finalize()方法

① 如果對象objA沒有重寫finalize()方法, 或者finalize ()方法已經被虛擬機調用過,則虛擬機視爲“沒有必要執行”,objA被判定爲不可觸及的(直接死)。
② 如果對象objA重寫 了finalize()方法,且還未執行過,那麼objA會被插入到F-Queue隊列中,由一-個虛擬機自動創建的、低優先級的Finalizer線程觸發其finalize()方法執行。
finalize()方法是對 象逃脫死亡的最後機會,稍後GC會對F-Queue隊列中的對象進行第二次標記。如果objA在finalize()方法中與引用鏈上的任何一一個對象建立了聯繫,那麼在第二次標記時,objA會被移出“即將回收”集合。之後,對象會再次出現沒有引用存在的情況。在這個情況下,finalize方法不會被再次調用,對象會直接變成不可觸及的狀態,也就是說,一個對象的finalize方法只會被調用一次。

垃圾清除階段算法

當成功區分出內存中存活對象和死亡對象後,GC接下來的任務就是執行垃圾回收,釋放掉無用對象所佔用的內存空間,以便有足夠的可用內存空間爲新對象分配內存。
目前在JVM中比較常見的三種垃圾收集算法是標記-清除算法( Mark-sweep)、複製算法(Copying)、標記-壓縮算法(Mark-Compact )。

標記-清除算法

標記-清除算法(Mark-Sweep)是一種非常基礎和常見的垃圾收集算法,該算法被J .McCarthy等人在1960年提出並並應用於Lisp語言。

執行過程

當堆中的有效內存空間(available memory) 被耗盡的時候,就會停止整個程序(也被稱爲stop the world) ,然後進行兩項工作,第一項則是標記,第二項則是清除

  • 標記: Collector從引用根節點開始遍歷,標記所有被引用的對象(注意:是非垃圾對象)。一般是在對象的Header中記錄爲可達對象。
  • 清除: Collector對堆內存從頭到尾進行線性的遍歷,如果發現某個對象在其Header中沒有標記爲可達對象,則將其回收。
    在這裏插入圖片描述

優缺點

優點:非常基礎和常見、易於理解
缺點:
➢ 效率不算高
➢ 在進行GC的時候,需要停止整個應用程序,導致用戶體驗差
➢ 這種方式清理出來的空閒內存是不連續的,產生內存碎片。需要維護一個空閒列表
注意:何爲清除?
➢ 這裏所謂的清除並不是真的置空,而是把需要清除的對象地址保存在空閒的地址列表裏。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠就存放。

複製算法

背景:
爲了解決標記-清除算法在垃圾收集效率方面的缺陷,M. L. Minsky於1963年發表了著名的論文,“ 使用雙存儲區的Li sp語言垃圾收集器CAPLISP Garbage Collector Algorithm Using Serial Secondary Storage )”。M. L.Minsky 在該論文中描述的算法被人們稱爲複製(Copying) 算法,它也被M. L. Minsky本人成功地引入到了Li sp語言的一個實現版本中。

核心思想

將活着的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,之後清除正在使用的內存塊中的所有對象,交換兩個內存的角色,最後完成垃圾回收。在survive1區和survive0區對象的複製其實就是採取的這種算法。
在這裏插入圖片描述

優缺點

優點:沒有標記和清除過程,實現簡單,運行高效。複製過去以後保證空間的連續性,不會出現“碎片”問題。

缺點:此算法的缺點也是很明顯的,就是需要兩倍的內存空間。對於G1這種分拆成爲大量region的GC,複製而不是移動,意味着GC需要維護region之間對象引用關係,不管是內存佔用或者時間開銷也不小。

應用場景

**特別的:如果系統中的垃圾對象很多,複製算法就不太理想,因爲複製算法需要複製的存活對象數量通常並不會太大,或者說非常低纔行。**否則需要複製的對象太多導致複製算法的優點變缺點。

在新生代,對常規應用的垃圾回收,一次通常可以回收70%- 99%的內存空間(新生代對象幾乎都是朝生夕死)。回收性價比很高。所以現在的商業虛擬機都是用這種收集算法回收新生代(年輕代往to區,to區往from區)。

標記壓縮算法

標記-壓縮(也叫標記-整理、Mark - Compact)算法
背景:
複製算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在新生代經常發生,但是在老年代,更常見的情況是大部分對象都是存活對象。如果依然使用複製算法,由於存活對象較多,複製的成本也將很高。因此,基於老年代垃圾回收的特性,需要使用其他的算法。

標記-清除算法的確可以應用在老年代中,但是該算法不僅執行效率低下,而且在執行完內存回收後還會產生內存碎片,所以JVM的設計者需要在此基礎之上進行改進。標記-壓縮(Mark - Compact) 算法由此誕生。
1970年前後,G. L. Steele 、C. J. Chene 和D.S. Wise 等研究者發佈標記-壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記-壓縮算法或其改進版本。

執行過程

第一階段和標記清除算法一樣,從根節點開始標記所有被引用對象
第二階段將所有的存活對象壓縮到內存的一端,按順序排放。之後,清理邊界外所有的空間。
在這裏插入圖片描述
標記-壓縮算法的最終效果等同於標記-清除算法執行完成後,再進行一次內存碎所整理,因此,也可以把它稱爲標記-清除-壓縮(Mark- Sweep-Compact)算法。

二者的本質差異在於標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收後的存活對象是一項優缺點並存的風險決策

可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此–來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。

優缺點

優點:
● 消除了標記-清除算法當中,內存區域分散的缺點,我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可。
● 消除了複製算法當中,內存減半的高額代價。
缺點:
● 從效率上來說,標記-整理算法要低於複製算法,,更低於標記-清除算法。
● 移動對象的同時,如果對象被其他對象引用,則還需要調整引用的地址。
● 移動過程中,需要全程暫停用戶應用程序。即: STW

總結對比

效率上來說,複製算法是當之無愧的老大,但是卻浪費了太多內存。
而爲了儘量兼顧上面提到的三個指標,標記-整理算法相對來說更平滑- -些,但是效率上不盡如人意,它比複製算法多了一個標記的階段,比標記-清除多了一個整理內存的階段。

Mark-Sweep Mark-compact Copying
速度 中等 最慢 最快
空間開銷 少(但會堆積碎片) 少(不堆積碎片) 通常需要活對象的2倍大小(不堆積碎片)
移動對象

分代收集算法

前面所有這些算法中,並沒有一種算法可以完全替代其他算法,它們都具有自己獨特的優勢和特點。分代收集算法應運而生。

分代收集算法,是基於這樣一個事實:不同的對象的生命週期是不一樣的。因此,不同生命週期的對象可以採取不同的收集方式,以便提高回收效率。一般是把Java分爲新生代和老年代,這樣就可以根據各個年代的特點使用不同的回收算法,以提高垃圾回收的效率。

在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鉤,因此生命週期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命週期會比較短,比如: String對象, 由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。

目前幾乎所有的GC都是採用分代收集(Generational Collecting) 算法執行垃圾回收的
在HotSpot中,基於分代的概念,GC所使 用的內存回收算法必須結合年輕代和老年代各自的特點。

  • 年輕代(Young Gen)|
    年輕代特點:區域相對老年代較小,對象生命週期短、存活率低,回收頻繁。
    這種情況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對象大小有關,因此很適用於年輕代的回收。而複製算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解(新生代:老年代=1:2,新生代中eden:s0:s1=8:1:1)。
  • 老年代(Tenured Gen)
    老年代特點:區域較大,對象生命週期長、存活率高,回收不及年輕代頻繁。
    這種情況存在大量存活率高的對象,複製算法明顯變得不合適。一般是由標記-清除或者是標記-清除與標記-整理的混合實現。
    ➢ Mark階段的開銷與存活對象的數量成正比。
    ➢ Sweep階段的開銷與所管理區域的大小成正相關。
    ➢ Compact階段的開銷與存活對象的數據成正比。

分代的思想被現有的虛擬機廣泛使用。幾乎所有的垃圾回收器都區分新生代和老年
代。

增量收集算法

上述現有的算法,在垃圾回收過程中,應用軟件將處於一種stop the World的狀態。在stop the world狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。爲了解決這個問題,即對實時垃圾收集算法的研究直接導致了增量收集( Incremental Collecting) 算法的誕生。

基本思想

如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那麼就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反覆,直到垃圾收集完成

總的來說,增量收集算法的基礎仍是傳統的標記-清除和複製算法。增量收集算法通過對線程間衝突的妥善處理,允許垃圾收集線程以分階段的方式完成標記、清理或複製工作

缺點

使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因爲線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升造成系統吞吐量的下降

分區算法

一般來說,在相同條件下,堆空間越大,一次GC時所需要的時間就越長,有關GC產生的停頓也越長。爲了更好地控制GC產生的停頓時間,將一切大的內存區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減少一次GC所產生的停頓。
分代算法將按照對象的生命週期長短劃分成兩個部分分區算法將整個堆空間劃分成連續的不同小區間region。每一個小區間都獨立使用,獨立回收。這種算法的好處是可以控制一次回收多少個小區間。
在這裏插入圖片描述

寫在最後

注意,這些只是基本的算法思路,實際GC實現過程要複雜的多,目前還在發展中的前沿GC都是複合算法,並且並行和併發兼備。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章