標籤:jvm垃圾收集 根搜尋演算法 java對象引用
一、關於Java記憶體回收的簡介
(1)Java 記憶體運行時地區的各個部分,其中程式計數器、虛擬機器棧、本地方法棧三個地區隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。
(2)每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的(儘管在運行期會由 JIT 編譯器進行一些最佳化),因此這幾個地區的記憶體配置和回收都具備確定性.在這幾個地區內不需要過多考慮回收的問題,因為方法結束或線程結束時,記憶體自然就跟隨著回收了。
(3)而 Java 堆和方法區則不一樣,一個介面中的多個實作類別需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於運行期間時才能知道會建立哪些對象,這部分記憶體的分配和回收都是動態,垃圾收集器所關注的是這部分記憶體。
二、記憶體回收的一種方法:引用計數演算法
我們在面試的時候,當問到如何判斷對象是否存活,我們也許會回答:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加 1;當引用失效時,計數器值就減 1;任何時刻計數器都為 0 的對象就是不可能再被使用的。
這個答案是正確的,但是Java語言中並沒有使用這種方法作為記憶體回收機制。引用計數演算法,判定效率也很高,在大部分情況下它都是一個不錯的演算法,不使用的原因,其中最主要的是它很難解決對象之間的相互循環參考的問題。
例如:
對象 objA 和 objB 都有欄位 instance,賦值令 objA.instance = objB 及 objB.instance = objA,除此之外,這兩個對象再無任何引用,實際上這兩個對象已經不可能再被訪問,但是它們因為互相引用著對方,導致它們的引用計數都不為 0,於是引用計數演算法無法通知 GC 收集器回收它們。
/** * testGC()方法執行後, objA 和 objB 會不會被 GC 呢? */public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 這個成員屬性的唯一意義就是占點記憶體,以便能在 GC 日誌中看清楚是否被回收過 */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; // 假設在這行發生 GC,那麼 objA 和 objB 是否能被回收? System.gc(); }}
從上邊的代碼運行結果中,意味著虛擬機器並沒有因為這兩個對象互相引用就不回收它們,這也從側面說明虛擬機器並不是通過引用計數演算法來判斷對象是否存活的。
三、根搜尋演算法
這裡都知道了,Java記憶體回收機制的話採用的是“根搜尋演算法”,來判斷對象是否存活,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈( Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達)時,則證明此對象是停用。
如所示,對象 object 5、 object 6、 object 7 雖然互相有關聯,但是它們到 GC Roots 是不可達的,所以它們將會被判定為是可回收的對象。
在 Java 語言裡,可作為 GC Roots 的對象包括下面幾種:
(1)虛擬機器棧(棧幀中的本地變數表)中的引用的對象。
(2)方法區中的類靜態屬性引用的對象。
(3)方法區中的常量引用的對象。
(4)本地方法棧中 JNI(即一般說的 Native 方法)的引用的對象。
四、引用的類型
我們對引用的理解也許很簡單,就是:如果 reference類型的資料中儲存的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。但是書上說的這種方式過於狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些“食之無味,棄之可惜”的對象就顯得無能為力。我們希望能描述這樣一類對象:當記憶體空間還足夠時,則能保留在記憶體之中;如果記憶體在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩衝功能都符合這樣的應用情境。
一般的參考型別分為強引用( Strong Reference)、軟引用( Soft Reference)、弱引用( Weak Reference)、虛引用( Phantom Reference)四種,這四種引用強度依次逐漸減弱。
下邊是四中類型的介紹:
(1)強引用就是指在程式碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
(2)軟引用用來描述一些還有用,但並非必需的對象。對於軟引用關聯著的對象,**在系統將要發生內
存溢出異常之前,將會把這些對象列進回收範圍之中並進行第二次回收。如果這次回收還是沒有足夠
的記憶體,才會拋出記憶體溢出異常**。在 JDK 1.2 之後,提供了 SoftReference 類來實現軟引用。
(3)弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。**當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用
關聯的對象**。在 JDK 1.2 之後,提供了 WeakReference 類來實現弱引用。
(4)虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種參考關聯性。一個對象是否有虛引用的存在,完全不會對其存留時間構成影響,也無法通過虛引用來取得一個對象執行個體。為一個對象設定虛引用關
聯的唯一目的就是希望能在這個對象被收集器回收時收到一個系統通知。在 JDK 1.2 之後,提供了PhantomReference 類來實現虛引用。
五、記憶體回收時候的兩次標記
在根搜尋演算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,**要真正宣
告一個對象死亡,至少要經曆兩次標記過程**:如果對象在進行根搜尋後發現沒有與 GC Roots 相串連
的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize()
方法。 **當對象沒有覆蓋 finalize()方法,或者 finalize()方法已經被虛擬機器調用過,虛擬機器將這兩種情況
都視為“沒有必要執行”。**
如果這個對象被判定為有必要執行 finalize()方法,那麼這個對象將會被放置在一個名為 F-Queue 的
隊列之中,並在稍後由一條由虛擬機器自動建立的、低優先順序的 Finalizer 線程去執行。這裡所謂的“執
行”是指虛擬機器會觸發這個方法,但並不承諾會等待它運行結束。這樣做的原因是,如果一個對象在
finalize()方法中執行緩慢,或者發生了死迴圈(更極端的情況),將很可能會導致 F-Queue 隊列中
的其他對象永久處於等待狀態,甚至導致整個記憶體回收系統崩潰。 finalize()方法是對象逃脫死亡命運
的最後一次機會,稍後 GC 將對 F-Queue 中的對象進行第二次小規模的標記,如果對象要在 finalize()
中成功拯救自己—只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己( this 關鍵字)賦
值給某個類變數或對象的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這
時候還沒有逃脫,那它就真的離死不遠了。
下邊一段代碼我們可以看到一個對象的 finalize()被執行,但是它仍然可以存活。
/** * 此代碼示範了兩點: * 1.對象可以在被 GC 時自我拯救。 * 2.這種自救的機會只有一次,因為一個對象的 finalize()方法最多隻會被系統自動調用一次. */public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize mehtod executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); // 對象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因為 Finalizer 方法優先順序很低,暫停 0.5 秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead"); } // 下面這段代碼與上面的完全相同,但是這次自救卻失敗了 SAVE_HOOK = null; System.gc(); // 因為 Finalizer 方法優先順序很低,暫停 0.5 秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead"); } }}
執行結果如下:
finalize mehtod executed!yes, i am still aliveno, i am dead
代碼中有兩段完全一樣的程式碼片段,執行結果卻是一次逃脫成功,一次失敗,這是因為任何一個對象的 finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的 finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了。
六、回收方法區
(以下是書上直接找的,不做理解)
很多人認為方法區(或者 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 查看類的載入和卸載資訊。
在大量使用反射、動態代理、 CGLib 等 bytecode 架構的情境,以及動態產生 JSP 和 OSGi 這類頻繁
自訂 ClassLoader 的情境都需要虛擬機器具備類卸載的功能,以保證永久代不會溢出。
Java虛擬機器之垃圾收集器(7)