對象是否已死
Java記憶體運作時區域的各個部分,其中程式計數器、虛拟機棧、本地方法棧3個區域随線程而生,随線程而滅:棧中的棧幀随着方法的進入和退出而有條不紊地執行着出棧和入棧操作。每一個棧幀中配置設定多少記憶體基本上是在類結構确定下來時就已知的(盡管在運作期會有JIT編譯器進行一些優化,但是在基于概念模型的讨論中基本可以認為是确定的),是以這幾個區域的記憶體配置設定與回收都具備确定性,這幾個區域内就不需要過多考慮垃圾回收的問題,因為方法結束或線程結束時,記憶體自然也就跟着回收了。而Java堆和方法區則不一樣,一個接口中的多個實作類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體可能也不一樣,我們隻有在程式運作期間才能知道會建立哪些對象,這部分的記憶體配置設定和回收都是動态的,垃圾收集器所關注的就是這部分記憶體。在Java堆中幾乎存放着所有的對象執行個體。垃圾收集器在對堆進行回收之前,第一件事情就是确定這些對象之中哪些還“存活”着,哪些已經“死去”(即不可能再被任何途徑使用的對象)。
引用計數算法
有些資料中判斷對象是否存活的算法是這樣的:給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器值都為0的對象就是不可能再被使用的。
客觀地講,引用計算法(Reference Counting)的實作簡單,判定效率也高,在大部分情況下它都是一個不錯的算法,但是它很難解決對象之間互相循環引用的問題。
可達性分析算法
在主流的商用程式語言(Java、C#)的主流實作中,都是通過可達性分析(Reachability Analysis)來判斷對象是否存活的。這個算法的基本思想就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些起始點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如下圖所示,對象object5和object6雖然有關聯,但是它們到GC Roots是不可達的,是以它們會被判定為是可回收對象。
在Java語言中,可作為GC Roots的對象包括以下幾種:
- 虛拟機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜态屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
再談引用
無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鍊是否可達,判斷對象是否存活都與“引用”有關。在JDK1.2之前,Java中引用的定義很傳統:如果refenrence類型的資料中存儲的數值代表的是另外一塊記憶體的起始位址,就稱為這塊記憶體代表着一個引用。這種定義很純粹,但是太過狹隘,一個對象在這種定義下隻有被引用或者沒有被引用兩證狀态,對于如何描述一些“食之無味,棄之可惜”的對象就顯得無能為力。我們希望能描述這樣一類對象:當記憶體空間還足夠時,則能保留在記憶體之中;如果記憶體空間在進行垃圾收集後還非常緊張,則可以抛棄這些對象。很多系統的緩存功能都符合這樣的應用場景。
在JDK1.2之後,Java對引用的概念進行了擴充,将引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
- 強引用就是指在代碼中普遍存在的,類似“Object obj = new Object()”這類的引用,隻要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
- 軟引用用來描述一些還有用但是并非必需的對象。對于軟引用關聯的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象列入可回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。在JDK1.2之後提供了SoftReference類來實作軟引用。
- 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻能存活到下次垃圾收集發生之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉被弱引用關聯的對象。在JDK1.2之後,提供了WeakReference類來實作弱引用。
- 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對它的生存時間産生影響,也無法通過一個虛引用來取得一個對象執行個體。為一個對象設定虛引用的唯一目的就是能夠在這個對象被垃圾收集器回收時收到一個系統通知。在JDK1.2之後,提供了PhantomReference類來實作虛引用。
生存還是死亡
即使是在可達性分析算法中的不可達對象,也并非是非死不可的,這時候它們暫時存于“緩刑”階段,要真正宣告一個對象死亡,至少要經曆兩次标記過程:如果對象在進行可達性分析後發現沒有與GC Roots相關聯的引用鍊,那它将會被進行第一次标記并且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當對象沒有覆寫finalize()方法或者finalize()方法已經被虛拟機調用過,虛拟機将這兩種情況都是為“沒有必要執行”。
如果這個對象被判定為有必要執行finalize()方法,那麼這個對象将會被放在一個叫做F-Queue的隊列之中,并在稍後由一個由虛拟機自動建立的、低優先級的Finalizer線程去執行它。這裡所謂的“執行”是指虛拟機會觸發這個方法,但并不承諾會等待它運作結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生的死循環(更極端的情況下),将很可能導緻F-Queue隊列中的其他對象處于等待狀态,甚至導緻整個垃圾收集系統崩潰。finalize()方法是對象逃脫被回收命運的最後一次機會,稍後GC将對F-Queue中的對象進行第二次小規模的标記,如果對象要在finalize()中成功拯救自己–隻需要重新與引用鍊上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)指派給某個類變量或者對象的成員變量,那在第二次标記時它将會被移除出“即将回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。
回收方法區
很多人認為方法區(或者HotSpot虛拟機中的永久代)是沒有垃圾收集的,Java虛拟機規範中确實說過可以不要求虛拟機在方法區實作垃圾收集,而且在方法區進行垃圾收集的“成本效益”一般比較低:在堆中,尤其是在新生代中,正常應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低于此。
永久代的垃圾收集主要回收兩部分内容:廢棄常量和無用類。回收廢棄常量與回收Java堆中的對象非常相似。以常量池中字面量的回收為例:假如一個字元串“abc”已經進入了常量池,但是目前系統中沒有任何一個String對象是叫做“abc”的,換句話說就是沒有任何一個String對象引用常量池中的“abc”常量,也沒有其他的地方引用了這個字面量,如果這時發生記憶體回收,而且必要的話,這個“abc”常量将會被系統清除出常量池。常量池中的其他類(接口)、方法、字段的符号引用也與此類似。
判斷一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻很多。類需要同時滿足下面3個條件才能算是“無用的類”:
- 該類的所有執行個體都已經被回收,也就是說Java堆中不存在該類的執行個體
- 加載該類的ClassLoader已經被回收
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射的方式通路該類的方法
虛拟機可以對滿足以上3個條件的類進行垃圾收集,這裡說的僅僅是可以,而并不是和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛拟機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:TraceClassUnLoading檢視類加載和解除安裝資訊。其中-verbose:class以及-XX:+TraceClassLoading可以在Product版的虛拟機中使用,而-XX:TraceClassUnLoading參數需要在FastDebug版的虛拟機支援。
在大量使用反射、動态代理、CGLib等ByteCode架構、動态生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛拟機具備類解除安裝功能,以保證永久代不會溢出。