Java中的垃圾收集器

總結自《深入理解Java虛擬機:JVM高級特性與最佳實踐》第3章

1、GC(Garbage Collection)需要完成的3件事:

  • 哪些內存需要回收?
  • 什麼時候回收?
  • 如何回收?

 

2、哪些內存需要回收?引申出如何判斷對象是“存活”,還是“死去(即不能再被任何途徑使用的對象)”:

    2.1 引用計數算法:給對象中添加一個引用計數器,每當有 一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0 的對象就是不可能再被使用的。

    缺點:很難解決對象之間相互循環引用的問題。

    

objA.instance=objB;

objB.instance=objA;

  除此之外,這兩個對象再無任何引用。實際上這兩個對象已經不可能再被訪問,
但是它們因爲互相引用着對方,導致它們的引 用計數都不爲0,於是引用計數算法無法通知GC收集器回收它們。

 

 2.2  可達性分析算法(基本都是這個算法判斷):這個算法的基本思 路就是通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所 走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連 (用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如 圖3-1所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達 的,所以它們將會被判定爲是可回收的對象。

    

 

    

2.3 在Java語言中,可作爲GC Roots的對象包括下面幾種: 

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

 

3、 Java4種引用分類(4種引用強度依次逐漸減弱。)

    強引用:在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引 用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

    軟引用:是用來描述一些還有用但並非必需的對象。對於軟引用關聯着的對象,在系統將 要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回 收還沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2之後,提供了SoftReference類來實 現軟引用。

    弱引用:用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的 對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠, 都會回收掉只被弱引用關聯的對象。在JDK 1.2之後,提供了WeakReference類來實現弱引 用。

    虛引用:也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引 用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一 個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在 JDK 1.2之後,提供了PhantomReference類來實現虛引用。

 

5、判斷一個類是否是“無用的類”:

  •    該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。 
  •    加載該類的ClassLoader已經被回收。 
  •    該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc 參數進行控制。

可以使用-verbose:class以及-XX:+TraceClassLoading、-XX: +TraceClassUnLoading查看類加載和卸載信息。

其中-verbose:class和-XX: +TraceClassLoading可以在Product版的虛擬機中使用,

-XX:+TraceClassUnLoading參數需要 FastDebug版的虛擬機支持。

 

6、垃圾收集算法

    6.1  標記-清除(Mark-Sweep)算法

        算法分 爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。

缺點:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

    

    6.2   複製(copying)算法 (回收新生代)

        它將可用內存按容 量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着 的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是 對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指 針,按順序分配內存即可,實現簡單,運行高效。

    缺點:將內存縮小爲了原 來的一半,代價太高。

         更關鍵的 是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。

 

    現在的商業虛擬機都採用這種收集算法來回收新生代。將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[1]。 當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另外一塊Survivor空間上,最 後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10% 的內存會被“浪費”。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每 次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這裏 指老年代)進行分配擔保(Handle Promotion)。

 

6.3   標記-整理算法(回收老年代)

    根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程 仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存 活的對象都向一端移動,然後直接清理掉端邊界以外的內存,“標記-整理”算法的示意圖如 圖3-4所示。

 

 

6.4  分代收集(Generational Collection)算法

    當前商業虛擬機的垃圾收集都採用“分代收集”(Generational Collection)算法,這種算 法並沒有什麼新的思想,只是根據對象存活週期的不同將內存劃分爲幾塊。一般是把Java堆 分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代 中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付 出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間 對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。

 

7、垃圾收集器

    如果說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

                JDK1.7 之後的虛擬機包含的所有收集器,存在連線說明可以搭配使用。虛擬機所處的區域,則表示它是屬於新生代收集器還是老年代收集器。

 

    

    7.1  Serial垃圾收集器

        ,這個收集器是一個單線程的收集器,但它 的“單線程”的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作, 更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。“Stop The World”這個名字也許聽起來很酷,但這項工作實際上是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應用來說都是難以接受的。    

            它是虛擬機運行在Client模式下的默認新生代收集器。

 

    7.2   ParNew收集器

        ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之 外,其他與Serial收集器相比並沒有太多創新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器。

        在談論垃圾收集器的上下文語境中,並行和併發可以解釋如下。

 ●並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀 態。

 ●併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能 會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另一個CPU上。

 

7.3    Parallel Scavenge收集器(“吞吐量優先”垃圾收集器)

        Parallel Scavenge收集器的目標則是達到 一個可控制的吞吐量(Throughput),同時還具備自適應調節策略。

        Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。

 

7.4   Serial Old收集器

    Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整 理”算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。如果在Server模式 下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge 收集器搭配使用[1],另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

 

7.5   Parallel Old收集器

    Parallel Old是Parallel Scavenge收集器的老年代版本,在1dk1.6提出來的。使用多線程和“標記-整理”算法。

    在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old 收集器

 

7.6  CMS(Concurrent Mark Sweep)收集器

    CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。

    從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基於“標記—清除”算法實現 的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分爲4個步驟,包括: 

  •     初始標記(CMS initial mark)
  •     併發標記(CMS concurrent mark)
  •      重新標記(CMS remark) 
  •     併發清除(CMS concurrent sweep)

 

    3個缺點: 

  •         CMS收集器對CPU資源非常敏感;
  •         CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次 Full GC的產生。
  •         由於其基於“標記—清除”算法實現的收集器,如果讀者對前面這種算法介紹還有印象的話,就可能想到這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有 很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。

 

    7.7  G1收集器

    如果不計算維護Remembered Set的操作,G1收集器的運作大致可劃分爲以下幾個步驟:

  •  初始標記(Initial Marking) 
  •  併發標記(Concurrent Marking)
  •  最終標記(Final Marking) 
  •  篩選回收(Live Data Counting and Evacuation)

8、新生代GC和老年代GC

新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因爲Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。 

    老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴 隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行 Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

 

 

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