1. 程式人生 > >垃圾回收策略(二)

垃圾回收策略(二)

垃圾回收:

即收集已經“死去”的物件。Java記憶體執行時資料區中程式計數器虛擬機器棧本地方法棧三個部分的隨執行緒而生,隨執行緒而滅。每個棧幀中分配多少記憶體在類結構確定時就是可知的,因此這三個區域的記憶體分配和回收都具備確定性,不需過多考慮回收問題,因為方法結束或執行緒結束時,記憶體就回收了。而Java堆和方法區則不一樣,一個介面中的多個實現類的記憶體可能不一樣,一個方法中的分支需要的記憶體也可能不同,只有在程式處於執行期間才能知道會建立哪些物件,這部分記憶體分配和回收都是動態的,垃圾收集器所關注的也是這部分記憶體。

判斷物件是否死去(可回收)

判斷演算法主要有兩種:

1.引用計數演算法(據說廢棄)。

2.可達性分析演算法。

Java是通過可達性分析來判斷物件是否存活的。

可達性分析演算法:通過一系列的稱為“GC Roots” 的物件作為起點,從這些節點開始向下搜尋,搜尋走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(GC Roots到這個物件不可達)時,則證明此物件是無用的。如圖:物件5、6、7雖然互有關聯,但是他們到GC Roots是不可達的,所以被判定為可回收物件。

Java語言中,可作為GC Roots的物件包括下面幾種:

1.虛擬機器棧中引用的物件。

2.方法區中靜態屬性引用的物件。

3.方法區中常量引用的物件

4.本地方法棧中JNI引用的物件。

引用:

        無論是通過引用計數演算法還是可達性分析演算法判斷物件的引用鏈是否可達判斷物件是否存活都與“引用”有關

        Java中的引用定義:如果reference型別的資料中儲存的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表這一個引用。這種規定過於狹隘,一個物件在這種定義下只有被分為引用或者沒有被引用兩種狀態。對於一些“食之無味,棄之可惜”的物件就顯得無能為力。我們希望能描述這樣一類物件:當空間還足夠時,則儲存在記憶體之中;如果記憶體空間在垃圾收集後還是很緊張,則可以拋棄這些物件。很多系統的快取功能都符合這樣的應用場景。JDK1.2之後Java對引用的概念進行了擴充,將引用分為強引用

(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

強引用:指程式中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉引用的物件。

軟引用:描述一些還有用但並非必須的物件。對於軟引用關聯的物件,在系統將要發生記憶體溢位異常以前,將會把這些物件進行回收,如果還是沒有足夠的記憶體,才會丟擲記憶體溢位異常。此特性可用於進行資料快取。如:SoftReference類。

弱引用:也是描述非必須物件,被弱引用關聯的物件只能生存到下一次收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。如:WeakReference類。

虛引用:也成為幽靈引用或幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的是能在這個物件被收集器回收時收到一個系統通知。如:PhantomReference類。

對於物件的“自我拯救”,Java中用finalize()實現,物件類可以重寫該方法實現一次救贖機會,但finalize()方法並不推薦使用

方法區的垃圾回收:

很多人認為方法區(HotSpot中的永久代)是沒有垃圾收集的,Java虛擬機器規範中確實不要求虛擬機器在方法區實現垃圾收集,而且在方法區中進行垃圾收集的“價效比”一般比較低。在堆中,尤其在新生代中,常規一次垃圾收集一般可以回收70~95%的空間,而永久代效率遠低於此。

永久代主要回收兩部分內容:

廢棄常量

無用的類

1.回收廢棄常量與Java堆中物件類似,以常量池中字面量回收為例,如果“abc”已經進入了常量池,如果系統沒有任何一個String物件叫做“abc”,如果有回收必要,“abc”會被系統清理出常量池。

2.判定一個類是否是“無用的類”且需要解除安裝類的條件則相對苛刻許多。需要滿足以下3個條件:

1)該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項。

2)載入該類的ClassLoader已經被回收。

3)該類對應的java.lang.Class物件沒有在任何地方引用,無法在任何地方通過反射訪問該類的方法。

在大量使用反射、動態代理、CGLIB等ByteCode框架、動態生成JSP以及PSGI這類頻繁自定義的ClassLoader的場景都需要虛擬機器具備類解除安裝的功能

HotSpot中的實現

可達性分析中會發生:

1.Stop The World。

2.列舉根節點。

Stop The World可達性分析對執行時間的敏感體現在GC停頓上,在整個分析期間,整個執行系統看起來就像凍結在某個時間點上,不能出現在分析過程中物件引用關係還在不斷變化的情況。這導致GC進行時,必須停頓所有Java執行執行緒(Stop The World)。即使在號稱(幾乎)不會發生停頓的CMS收集器中,列舉根節點時也必須要停頓

至於為什麼在GC時要發生STW,有一個很適合的比喻:你媽媽在給你打掃房間的時候,肯定讓你在老實呆著,否則她一邊打掃,你一邊扔紙屑,這房間還能打掃完?

列舉根節點:簡言之就是列舉出所有“GC Roots”。在可達性分析中,通過GC Roots 節點找引用鏈判斷物件在鏈情況。而可以作為GC Roots的節點主要是全域性性引用(常量、類變數)與執行上下文(棧幀中的本地變量表)中,現在很多應用僅方法區就有數百兆。如果要逐個檢查GC Roots節點,那必然會消耗很多時間。目前主流Java虛擬機器使用的都是準確式GC,所以在執行系統停頓下來後,並不需要一個不漏的檢查所有執行上下文和全域性引用的位置,虛擬機器應當是有辦法直接得知哪些地方存放著物件引用。在HotSpot的實現中,是使用一組稱為OopMap的資料結構來達到這個目的的。在類載入完成時,HotSpot就把物件內具體偏移量上是什麼型別的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些資訊。

安全點:在Oop的協助下HotSpot可快速完成GC Roots列舉,但有一個現實的問題,Oop內容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高。事實上,HotSpot的確沒有為每條指令都生成OopMap,而只在“特定的位置”記錄這些資訊,這些位置稱為安全點(Safepoint),即程式執行時並非在任何地方都能停頓下來開始GC,只有達到安全點才能暫停。SafePoint的選定既不能太少以至於讓GC等待太長,也不能過於頻繁以至於過分增大執行時負荷。所以安全點的選定是以程式“是否具有讓程式長時間執行的特徵”。如:方法呼叫、迴圈跳轉、異常跳轉等,所以具有這些功能的指令才會產生SafePoint。對於SafePoint,如何在GC發生時讓所有執行緒,都跑到最近的安全點上再停頓。現在有兩種方案可供選擇:

1.搶先式中斷

2.主動式中斷

1)搶斷式中斷:不需要執行緒配合,在GC發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓其跑到安全點上。現在幾乎沒有虛擬機器實現採用搶先式中斷來暫停執行緒從而響應GC事件。

2)主動式中斷,不直接對執行緒操作,設定一個標誌位,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起輪詢標誌的地方和安全點是重合的

安全區域:使用SafePoint似乎已經完美的解決了如何進入GC的問題,但實際情況卻並不一定。SafePiont機制保證了程式執行時,在不太長的時間內就會遇到可以進入的GC的SafePoint。但當程式“不執行”的時候,也就是執行緒沒有分配CPU時間,典型的例子就是執行緒處於Sleep狀態或者Blocked狀態,這時候執行緒無法響應JVM的中斷請求,“跑”到安全的地方去中斷掛起JVM顯然不可能等待執行緒重新分配CPU時間。對於這種情況,就需要安全區域(Safe Resgion)來解決。安全區域是指在一段程式碼之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的,我們也可以把SafeRegion看做事被擴充套件了的SafePoint。在執行緒執行到了Safe Region中的程式碼時,首先標記自己已經進入了Safe Region。那樣,在這段時間裡JVM要發起GC時,就不用管標記已為Safe Region狀態的執行緒了,在執行緒要離開Safe Region時,首先會去查詢是否可以離開的標誌位,如果已完成GC或者未GC則可以安全退出,否則它就必須等到收到可以安全離開的訊號為止。