概述
說到垃圾收集(Garbage Collection,GC),大部分人都會把這項技術當做 Java 語言的伴生產物。事實上,GC的歷史比Java久遠,1960年誕生于MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期時,人們就在思考GC需要完成的3件事情:
- 哪些內存需要回收?
- 什么時候回收?
- 如何回收?
經過半個多時間的發展,目前內存的動態分配與內存回收技術已經相當的成熟,一切看起來都進入了“自動化”時代,那為什么我們還要去了解GC和內存分配呢?答案很簡單:當需要排查各種內存溢出、內存泄漏問題時,當垃圾收集成為系統達到更高并發量的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。
在 深入理解JVM虛擬機 - JVM運行時數據區 中,介紹了JVM內存運行時區域的各個部分,其中 程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅,因此在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨者回收了。而Java 堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處于運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的就是這部分內存,本文后續討論中的“內存”分配與回收也僅指這一部分內存。
如何判定對象已死亡
對象“存活”判定算法
在堆里面存放著Java世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”著,哪些已經“死亡”(即不可能被任何途徑使用的對象)。
1、引用計數算法
原理:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器加1;引用失效時,計數器減1;任何時刻計數器為0的對象就是不可能再被使用的。 缺點:很難解決對象相互循環引用的問題(兩個對象相互循環引用,但其實他們都已經沒有用了)。
2、可達性分析算法
在主流的商用程序語言(Java、C#、Lisp)的主流實現中都是通過可達性分析(Reachability Analysis)來判定對象是否存活的。
原理:通過一些列稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- 方法區中類靜態屬性引用的對象。
- 方法區中常量引用的對象。
- 本地方法棧中JNI(即一般說的Native方法)引用的對象。
再談引用
無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關。
在JDK1.2之后,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種應用強度依次逐漸減弱。
強引用
強引用就是指在程序代碼之中普遍存在的,類似“Object object = new Object()
”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用
軟引用是用來描述一些還在用但并非必需的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收,如果這次回收完成還沒有足夠的內存,才會拋出內存溢出異常。在JDK1.2之后,提供了SoftReference類來實現軟引用。
弱引用
弱引用也是用來描述非必需對象的,但是它的強度比軟引用要更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK1.2之后,提供了WeakReference類來實現弱引用。
虛引用
虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2之后,提供了PhantomReference類來實現虛引用。
回收方法區
很多人認為方法區(或者Hotspot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規范中確實說過可以不要求虛擬機在方法區實現垃圾收集,而且在方法區中進行垃圾收集的"性價比" 一般比較低:在堆中,尤其是新生代中,常規應用進行一次垃圾收集一般可以回收70% ~ 95%的空間,而永久代的垃圾收集效率遠低于此。
永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。
垃圾收集算法
1、標記-清除算法(Mark-Sweep)
原理:
標記-清除算法(Mark-Sweep)分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
缺點:
1.效率問題,標記和清除兩個過程的效率都不高;
2.空間問題,標記清除后會產生大量的不連續的內存碎片,空間碎片太多可能會導致以后再程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
2、復制算法
原理:
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已經使用過的內存一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
缺點:
需要復制,效率降低、浪費空間。
現在的商業虛擬機都采用這種收集算法來回收新生代。IBM公司的專門研究表明,新生代中對象98%都是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。
3、標記-整理算法(Mark-Compact)
原理: 標記過程任然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然后直接清理掉端邊界以外的內存。
分代收集算法(Generational Collection)
分代的垃圾回收策略,是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。
4、分代收集算法
當前的商業虛擬機的垃圾收集都采用“分代收集”(Generational Collection) 算法,這種算法并沒有什么新的思想,只是根據對象存活周期的不同將內存劃分為幾塊。
一般是把 Java 堆分為 年輕代 和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
在年輕代中,每次垃圾收集時都發現有大批的對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或者 **“標記-整理” **算法來進行回收。
垃圾收集器
如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現。Java虛擬機規范對垃圾收集器應該如何實現并沒有任何規定,因此不同的廠商,不同版本的虛擬機所提供的垃圾收集器都可能會有很大差別,并且一般都會提供參數供用戶根據自己的應用特點和要求組合出各個年代所使用的收集器。這里討論的收集器基于JDK1.7 Update 14之后的HotSpot虛擬機(在這個版本中正式提供了商用的G1收集器,之前G1仍處于試驗狀態),這個虛擬機包含的所有收集器如下圖所示:
上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。收集器所處的區域,則表示它是屬于新生代收集器還是老年代收集器。
在介紹這些收集器各種的特性之前,我們先來明確一個觀點:雖然我們是在對各個收集器進行比較,但并非為了挑選出一個最好的收集器。因為直到目前為止還沒有最好的收集器出現,更加沒有萬能的收集器,所以我們選擇的只是對具體應用最合適的收集器。
1、Serial收集器
它是一個單線程的收集器,但它的“單線程”的意義并不僅說明它只會使用一個CPU或者一條收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
2、ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣。
3、Parallel Scavenge收集器
Parallel Scavenge收集器是一個新時代收集器,它也是使用復制算法的收集器,又是并行的多線程收集器。
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能的縮短垃圾收集時用戶線程暫停的時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughout)。所謂吞吐量就是CPU運行用戶代碼的時間和總耗時的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。
4、Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。
5、Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本。
6、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。
7、G1收集器
G1收集器