讀書筆記 ---- 《深入理解Java虛擬機器》---- 第2篇:垃圾回收演算法
上一篇:Java記憶體區域與記憶體溢位異常:https://blog.csdn.net/pcwl1206/article/details/83990008
第2篇:垃圾回收演算法
首先明確GC需要完成的3件事情:
1、哪些記憶體需要回收?
2、什麼時候回收?
3、如何回收?
Java虛擬機器的執行時資料區中,程式計數器、虛擬機器棧和本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來就已知的,因此這三個區域的記憶體分配和回收都具有確定性,在幾個區域內就不需要過多考慮回收的問題,因為方法結束或者執行緒結束時,記憶體自然就跟隨著回收了。
而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體有可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於執行期才知道會建立哪些物件,這部分記憶體的分配和回收都是動態的。垃圾回收關注的就是這部分記憶體。
一、判斷物件是否存活的演算法
常用的有:引用計數法、可達性分析演算法
1、引用計數法
演算法思想:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器就減1;任何時刻計數器為0的物件就是不可能再被使用的。
引用計數法的實現簡單,判斷效率也很高,在大部分情況下都是一個不錯的演算法,但是引用計數法對於物件之間的相互迴圈引用的問題難以解決,因此Java虛擬機器中並未採用這種方法。
2、可達性分析演算法
演算法思想:通過一系列的稱為“GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連,即從GC Roots到這個物件不可達時,則證明此物件是不可用的。
如下圖所示的object5、object6、object7到GC Roots都是不可達的,所以他們會被判定為是可回收的物件。
在Java語言中,可作為GC Roots的物件包括下面幾種:
- 虛擬機器棧(棧中的本地變量表)中引用的物件;
- 方法區中類靜態屬性引用的物件;
- 方法區中常量引用的物件;
- 本地方法棧中JNI(即一般說的Native)引用的物件。
3、再談引用
無論是通過引用計數法判斷物件的引用數量,還是通過可達性分析演算法判斷物件的引用鏈是否可達,判斷物件是否存活都與“引用”有關。
我們希望能夠描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體之中;如果記憶體空間在進行垃圾收集後還非常緊張,則可以拋棄這些物件。很多系統的快取功能都符合這樣的場景。
Java將引用分為強引用、軟引用、弱引用和虛引用4種,這4種引用強度依次逐漸減弱。
- 強引用:就是指在程式程式碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的物件;
- 軟引用:用來描述一些還有用但並非必需的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常;
- 弱引用:也是用來描述非必須物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾回收前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件;
- 虛引用:稱為幽靈引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。
4 生存還是死亡
即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候他們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且第一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過了,虛擬機器將這兩種情況視為“沒有必要執行”。
如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會被放置在一個叫做F-Queue的對列之中,並在稍後由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模標記,如果物件要在finalize()中成功拯救自己----只要重新與引用鏈上的任何一個物件建立關聯即可,比如:把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那再第二次標記時它將被移除“即將回收”的集合;如果這個時候還沒有逃脫,那基本上它就真的被回收了。
5、回收方法區
方法區在HotSpot虛擬機器中被稱為永久代,很多人認為該部分記憶體是沒有垃圾回收的,Java虛擬機器也沒有對此做出規定,但是方法區中的廢棄常量和無用的類還是要回收的以保證永久代不會發生記憶體溢位。
在方法區中進行垃圾收集的“價效比”一般比較低,在堆中,尤其在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集器效率遠低於此。
廢棄常量:例如:常量池中一個字串“abc”,但是沒有任何String物件引用它,也沒有其他地方引用它;
無用的類:需要滿足下面3個條件:
1、該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項;
2、載入該類的ClassLoader已經被回收;
3、該類所對應的java.lang.Class物件在沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
二、垃圾回收演算法
1、標記 — 清除演算法
最基礎的收集演算法是“標記—清除”(Mark-Sweep)演算法,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。標記的過程在上面【4生存還是死亡】中講過了。
兩個不足:
1、效率問題:標記和清除兩個過程的效率都不高;
2、空間問題:標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
後面要講的垃圾回收演算法也都是基於標記—清除演算法的思路並對其不足進行改進而得到的。
2、複製演算法
為了解決效率問題,“複製”演算法出現了,它將可用記憶體按照容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可。
缺點:這種演算法的代價是將記憶體縮小為原來的一半,代價比較高。
現在的商業虛擬機器都採用這種收集演算法來回收新生代,IBM公司的專門研究表明,新生代中的物件98%是“朝生夕死”的,所以並不需要按照1:1的比例來劃分記憶體空間,而是將記憶體劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的物件一次性地複製到另外一個Survivor空間上,最後清理掉Eden和剛才使用過的Survivor空間。HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%,只有10%的記憶體會被“浪費”。
當然98%的物件可回收只是一般場景下的依據,沒有辦法保證每次回收都不多於10%的物件存活,當Survivor空間不夠用的時候,需要依賴老年代進行分配擔保,即:如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代。
3、標記—整理演算法
複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。
根據老年代的特點,提出了“標記—整理”演算法,標記過程仍與“標記—清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
4、分代收集演算法
根據記憶體中物件的存活週期不同,將記憶體劃分為幾塊,Java的虛擬機器中一般把堆記憶體劃分為新生代和老年代,當建立物件時一般在新生代中分配記憶體空間,當新生代垃圾收集器回收幾次之後依然存活的物件會被移動到老年代記憶體中,當大物件在新生代中無法找到足夠的連續記憶體時,也直接在老年代中建立。
現在的Java虛擬機器根據新生代和老年代的特點選擇最合適的垃圾回收演算法。
如上圖所示的Java堆記憶體:需要說明的是永久代空間Perm在Java8中已經被移除了。
- 新生代(Young Generation)
- Eden空間(Eden space,任何例項都通過Eden空間進入執行時記憶體區域)
- S0 Survivor空間(S0 Survivor space,存在時間長的例項將會從Eden空間移動到S0 Survivor空間)
- S1 Survivor空間 (存在時間更長的例項將會從S0 Survivor空間移動到S1 Survivor空間)
- 老年代(Old Generation)例項將從S1提升到Tenured(終身代)
- 永久代(Permanent Generation)包含類、方法等細節的元資訊
1 新生代
新生代使用複製和標記-清除垃圾收集演算法,研究表明,新生代中98%的物件是朝生夕死的短生命週期物件,所以不需要將新生代劃分為容量大小相等的兩部分記憶體,而是將新生代分為Eden區,Survivor from和Survivor to三部分,其佔新生代記憶體容量預設比例分別為8:1:1,其中Survivor from和Survivor to總有一個區域是空白,只有Eden和其中一個Survivor總共90%的新生代容量用於為新建立的物件分配記憶體,只有10%的Survivor記憶體浪費,當新生代記憶體空間不足需要進行垃圾回收時,仍然存活的物件被複制到空白的Survivor記憶體區域中,Eden和非空白的Survivor進行標記-清理回收,兩個Survivor區域是輪換的。
新生代中98%情況下空白Survivor都可以存放垃圾回收時仍然存活的物件,2%的極端情況下,如果空白Survivor空間無法存放下仍然存活的物件時,使用記憶體分配擔保機制,直接將新生代依然存活的物件複製到年老代記憶體中,同時對於建立大物件時,如果新生代中無足夠的連續記憶體時,也直接在年老代中分配記憶體空間。
Java虛擬機器對新生代的垃圾回收稱為Minor GC,次數比較頻繁,每次回收時間也比較短。
使用java虛擬機器-Xmn引數可以指定新生代記憶體大小。
2 老年代
老年代中的物件一般都是長生命週期物件,物件的存活率比較高,因此在老年代中使用標記-整理垃圾回收演算法。
Java虛擬機器對老年代的垃圾回收稱為MajorGC/Full GC,次數相對較少,每次回收的時間比較長。
當新生代中無足夠空間為物件建立分配記憶體,老年代中記憶體記憶體回收也無法回收到足夠的記憶體空間,並且新生代和老年代空間無法再擴充套件時,堆就會產生OutOfMemoryError異常。
Java虛擬機器用-Xms引數可以指定堆最小記憶體大小,-Xmx引數可以指定堆最大記憶體大小,這兩個引數分別減去Xmn引數指定的新生代記憶體大小,可以計算出老年代最小和最大記憶體容量。
3 永久代
Java虛擬機器記憶體中的方法區在Sun HotSpot虛擬機器中被稱為永久代,是被各個執行緒共享的記憶體區域,它用於儲存被虛擬機器載入的類資訊、常量、靜態變數、即時編譯後的程式碼等資料。永久代垃圾回收比較少,效率也比較低,但是永久代(方法區)中的廢棄常量和無用的類還是要回收的以保證永久代不會發生記憶體溢位。
永久代也使用標記—整理演算法進行垃圾回收,Java虛擬機器引數:-XX:PermSize和-XX:MaxPermSize可以設定永久代的初始大小和最大容量。
三 啟動Java垃圾回收
作為一個自動的過程,程式設計師不需要在程式碼中顯示地啟動垃圾回收過程。System.gc()
和Runtime.gc()
用來請求JVM啟動垃圾回收。
雖然這個請求機制提供給程式設計師一個啟動 GC 過程的機會,但是啟動由 JVM負責。JVM可以拒絕這個請求,所以並不保證這些呼叫都將執行垃圾回收。啟動時機的選擇由JVM決定,並且取決於堆記憶體中Eden區是否可用。JVM將這個選擇留給了Java規範的實現,不同實現具體使用的演算法不盡相同。
毋庸置疑,垃圾回收過程是不能被強制執行的。
四 什麼是Stop the World
Java中Stop-The-World機制簡稱STW,是在執行垃圾收集演算法時,Java應用程式的其他所有執行緒都被掛起(除了垃圾收集幫助器之外)。Java中一種全域性暫停現象,全域性停頓,所有Java程式碼停止,native程式碼可以執行,但不能與JVM互動;這些現象多半是由於gc引起。
GC時的Stop the World(STW)是大家最大的敵人。但可能很多人還不清楚,除了GC,JVM下還會發生停頓現象。
JVM裡有一條特殊的執行緒--VM Threads,專門用來執行一些特殊的VM Operation,比如分派GC,thread dump等,這些任務,都需要整個Heap,以及所有執行緒的狀態是靜止的,一致的才能進行。所以JVM引入了安全點(Safe Point)的概念,想辦法在需要進行VM Operation時,通知所有的執行緒進入一個靜止的安全點。
除了GC,其他觸發安全點的VM Operation包括:
1. JIT相關,比如Code deoptimization, Flushing code cache ;
2. Class redefinition (e.g. javaagent,AOP程式碼植入的產生的instrumentation) ;
3. Biased lock revocation 取消偏向鎖 ;
4. Various debug operation (e.g. thread dump or deadlock check);
上一篇:Java記憶體區域與記憶體溢位異常:https://blog.csdn.net/pcwl1206/article/details/83990008
參考及推薦:
1、JVM垃圾回收基本原理和演算法:https://blog.csdn.net/a724888/article/details/77981592
2、垃圾回收演算法:https://blog.csdn.net/chjttony/article/details/7883068