GC垃圾回收機制(垃圾檢測-回收算法)

垃圾檢測 回收算法

垃圾收集器一般必須完成兩件事:檢測出垃圾(對象死亡不可訪問);回收垃圾。怎麼檢測出垃圾?

垃圾檢測一般有引用計數法和可達性分析

引用計數法:給一個對象添加引用計數器,每當有個地方引用它,計數器就加1;引用失效就減1。

好了,問題來了,如果我有兩個對象object1和object2,互相引用,除此之外,沒有其他任何對象引用它們,實際上這兩個對象已經無法訪問,即是我們說的垃圾對象。但是互相引用,計數不爲0,導致無法回收

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

爲了解決這個問題,在Java中採取了 可達性分析法。該方法的基本思想是通過一系列的“GC Roots”對象作爲起點進行搜索,如果在“GC Roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的,不過要注意的是被判定爲不可達的對象不一定就會成爲可回收對象。被判定爲不可達的對象要成爲可回收對象必須至少經歷兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成爲可回收對象的可能性,則基本上就真的成爲可回收對象了。

可達性分析算法:以根集對象爲起始點進行搜索,如果有對象不可達的話,即是垃圾對象。這裏的根集一般包括java棧中引用的對象、方法區常良池中引用的對象

GC Roots 可理解爲「堆外指向堆內的引用」。在 Java 技術體系中,固定可作爲 GC Roots 的對象包括以下幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類靜態屬性引用的對象(比如引用類型的靜態變量)
  • 方法區中常量引用的對象(比如字符串常量池的引用)
  • 本地方法棧中 Native 方法引用的對象
  • JVM 內部的引用(例如:基本數據類型的 Class 對象、常駐異常、系統類加載器)
  • 同步鎖(synchronized 關鍵字)持有的對象
  • 反應 JVM 內部情況的 JMXBean、JVMTI 中註冊的回調、本地代碼緩存等
垃圾回收算法

在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裏面涉及到一個問題是:如何高效地進行垃圾回收。由於Java虛擬機規範並沒有對如何實現垃圾收集器做出明確的規定,因此各個廠商的虛擬機可以採用不同的方式來實現垃圾收集器,所以在此只討論幾種常見的垃圾收集算法的核心思想。
  1.Mark-Sweep(標記-清除)算法
  這是最基礎的垃圾回收算法,之所以說它是最基礎的是因爲它最容易實現,思想也是最簡單的。標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。具體過程如下圖所示:
在這裏插入圖片描述
優缺點分析:
主要優點:實現簡單;
主要缺點:
執行效率不穩定,標記和清除過程效率都不高(標記對象較多時);
內存空間碎片化問題(碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作)。
圖片來自:http://www.cnblogs.com/dolphin0520/p/3783345.html
 
 2.Copying(複製)算法
  爲了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。具體過程如下圖所示:
  在這裏插入圖片描述

優點:實現簡單,運行高效且不易產生內存碎片;
缺點:可用內存縮小爲原來的一半,空間浪費太多。

很顯然,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。

PS:一般不需要按照 1:1 的比例劃分內存空間,而是將內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活的對象一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。
HotSpot 虛擬機默認 Eden 和 Survivor 的大小比例是 8:1 (即“浪費”了 10% 的新生代空間)。
  3.Mark-Compact(標記-整理)算法
  爲了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。具體過程如下圖所示:
在這裏插入圖片描述
主要問題:

移動存活對象:回收內存時會更復雜(Stop The World);
不移動存活對象:分配內存時會更復雜(空間碎片問題)。

分代收集算法:
這是當前商業虛擬機常用的垃圾收集算法。分代的垃圾回收策略,是基於這樣一個事實:不同的對象的生命週期是不一樣的。因此,不同生命週期的對象可以採取不同的收集方式,以便提高回收效率。

爲什麼要運用分代垃圾回收策略?在java程序運行的過程中,會產生大量的對象,因每個對象所能承擔的職責不同所具有的功能不同所以也有着不一樣的生命週期,有的對象生命週期較長,比如Http請求中的Session對象,線程,Socket連接等;有的對象生命週期較短,比如String對象,由於其不變類的特性,有的在使用一次後即可回收。試想,在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,那麼消耗的時間相對會很長,而且對於存活時間較長的對象進行的掃描工作等都是徒勞。因此就需要引入分治的思想,所謂分治的思想就是因地制宜,將對象進行代的劃分,把不同生命週期的對象放在不同的代上使用不同的垃圾回收方式。

如何劃分?將對象按其生命週期的不同劃分成:年輕代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。其中持久代主要存放的是類信息,所以與java對象的回收關係不大,與回收息息相關的是年輕代和年老代。這裏有個比喻很形象

“假設你是一個普通的 Java 對象,你出生在 Eden 區,在 Eden 區有許多和你差不多的小兄弟、小姐妹,可以把 Eden 區當成幼兒園,在這個幼兒園裏大家玩了很長時間。Eden 區不能無休止地放你們在裏面,所以當年紀稍大,你就要被送到學校去上學,這裏假設從小學到高中都稱爲 Survivor 區。開始的時候你在 Survivor 區裏面劃分出來的的“From”區,讀到高年級了,就進了 Survivor 區的“To”區,中間由於學習成績不穩定,還經常來回折騰。直到你 18 歲的時候,高中畢業了,該去社會上闖闖了。於是你就去了年老代,年老代裏面人也很多。在年老代裏,你生活了 20 年 (每次 GC 加一歲),最後壽終正寢,被 GC 回收。有一點沒有提,你在年老代遇到了一個同學,他的名字叫愛德華 (慕光之城裏的帥哥吸血鬼),他以及他的家族永遠不會死,那麼他們就生活在永生代

年輕代:是所有新對象產生的地方。年輕代被分爲3個部分——Enden(翻譯:樂園)區和兩個Survivor(翻譯:倖存者)區(From和to)當Eden區被對象填滿時或jvm空閒,就會執行Minor GC。並把所有存活下來的對象轉移到其中一個survivor區(假設爲from區)。當from區填滿就會執行Minor GC,同樣會檢查存活下來的對象,並把它們轉移到另一個survivor區(假設爲to區)。這樣在一段時間內,總會有一個空的survivor區(from區或to區,在填滿時會進行Minor GC,存活下來的對象會轉移到另一個區from或to)。經過多次GC週期後,仍然存活下來的對象會被轉移到年老代內存空間。通常這是在年輕代有資格提升到年老代前通過設定年齡閾值來完成的。需要注意,Survivor的兩個區是對稱的,沒先後關係,from和to是相對的。

年老代:在年輕代中經歷了N次(默認15次)回收後仍然沒有被清除的對象,就會被放到年老代中,可以說他們都是久經沙場而不亡的一代,都是生命週期較長的對象。對於年老代和永久代,就不能再採用像年輕代中那樣搬移騰挪的回收算法,因爲那些對於這些回收戰場上的老兵來說是小兒科。通常會在老年代內存被佔滿時將會觸發Full GC,回收整個堆內存。

持久代:用於存放靜態文件,比如java類、方法等。持久代對垃圾回收沒有顯著的影響。 (在jdk1.8之後,移除了持久帶,取而代之的是 Metaspace(元空間)元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制)

補充:
Eden和Survivor的比例分配等
默認比例8:1。
大部分對象都是朝生夕死。
複製算法的基本思想就是將survivor區內存分爲兩塊(from/to),每次只用其中一塊,當這一塊內存用完就會進行GC垃圾回收,將還活着的對象複製到另外一塊上面。複製算法不會產生內存碎片。

jvm中一次完整的GC流程(從ygc到fgc)是怎樣的,重點講講對象如何晉升到年老代

對象優先在年輕代區中分配,若沒有足夠空間,Minor GC;大對象(需要大量連續內存空間)直接進入年老代;長期存活的對象進入持久代。如果對象在年輕代出生並經過第一次MGC後仍然存活,年齡+1,若年齡超過一定限制(15),則被晉升到年老代

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