JVM垃圾回收演算法的概念與分析
前言
在JVM記憶體模型中會將堆記憶體劃分新生代、老年代兩個區域,兩塊區域的主要區別在於新生代存放存活時間較短的物件,老年代存放存活時間較久的物件,除了存活時間不同外,還有垃圾回收策略的不同,在JVM中中有以下回收演算法:
- 標記清除
- 標記整理
- 複製演算法
- 分代收集演算法
有了垃圾回收演算法,那JVM是如果確定物件是垃圾物件的呢?判斷物件是否存活JVM也會有幾套自己判斷演算法了:
- 引用記數
- 可達性分析
有了垃圾回收和判斷物件存在這兩個概念後,再來逐步分析它們。
JVM是如何判斷物件是否存活的?
要是讓開發人員來判斷一個物件是否有用是很簡單的,簡單的說就是:物件沒有任何引用就認為該物件可以被回收了。假設有如下程式程式碼:
public class App { public static void main(){ checkFile("/"); } public static boolean checkFile(String path ){ File file = new File(path); return file.exists(); } }
程式執行起來在呼叫checkFile的時候JVM圖大概像這樣:
到checkFile方法執行完成之後,它裡面的區域性變數file就會隨著棧幀一起被清理,這個時候還存活在JVM堆中的File物件也是無用的了:
要是人為來判斷非常清晰的就發現File物件已經無用了,那換成JVM它又是如何來判斷物件是否能存活的呢?
引用記數
引用記數演算法原理比較簡單,想象下有個物件它有一個count屬性,每次引用該物件都會使count加1,假設JVM在判斷該物件是否存活的時候去檢查這個count屬性,發現這個屬性不為0說明還有其他物件在引用該物件。
等到checkFile方法執行完之後count就會減1變成0:
這樣一來JVM就很容易判斷一個物件是否存活了。
但是引用記數有一個明顯的缺點,就是無法解決迴圈引用的問題比如:A --> B --> A 這樣的物件關係它是沒有辦法來判斷物件是否該不該回收的。
GC Root(可達性分析)
為什麼會被稱為可達性分析演算法呢?可以這樣理解如果通過GC Root能到達一個物件那麼這個物件就是存活的。那什麼樣的物件才是GC Root呢?
在Java語言中,可作為GC Roots的物件包括下面幾種:
- 虛擬機器棧中引用的物件(棧幀中的本地變量表);
- 方法區中類靜態屬性引用的物件;
- 方法區中常量引用的物件;
- 本地方法棧中JNI(Native方法)引用的物件。
還是用上面的例子,在checkFile方法執行時,因為棧幀變數file可做為GC Root所以在執行期間JVM是絕對不會回收掉這個File物件:
但是等到checkFile執行完成之後,這個棧幀會被彈出,其中的變數也會被釋放,相應的沒有GC Root能到達堆中的File物件,這個時候就可以判斷這個物件是一個無用的物件了,然後安全回收。
垃圾收回演算法
標記清除
這種演算法分兩分:標記、清除兩個階段,
標記階段是從根集合(GC Root)開始掃描,每到達一個物件就會標記該物件為存活狀態,清除階段在掃描完成之後將沒有標記的物件給清除掉。
用一張圖說明:
這個演算法有個缺陷就是會產生記憶體碎片,如上圖B被清除掉後會留下一塊記憶體區域,如果後面需要分配大的物件就會導致沒有連續的記憶體可供使用。
標記整理
標記整理就沒有記憶體碎片的問題了,也是從根集合(GC Root)開始掃描進行標記然後清除無用的物件,清除完成後它會整理記憶體。
這樣記憶體就是連續的了,但是產生的另外一個問題是:每次都得移動物件,因此成本很高。
複製演算法
複製演算法會將JVM推分成二等分,如果堆設定的是1g,那使用複製演算法的時候堆就會有被劃分為兩塊區域各512m。給物件分配記憶體的時候總是使用其中的一塊來分配,分配滿了以後,GC就會進行標記,然後將存活的物件移動到另外一塊空白的區域,然後清除掉所有沒有存活的物件,這樣重複的處理,始終就會有一塊空白的區域沒有被合理的利用到。
兩塊區域交替使用,最大問題就是會導致空間的浪費,現在堆記憶體的使用率只有50%。
分代回收
新生代回收
JVM的堆分為新生代和老年代,兩種型別有不同的特性,根據它們的特性來選擇不同的回收演算法,這種演算法會將新生代劃分為一塊Eden和二個Survivor區:
如上面的圖有三塊區域它們會按照8:1:1的比例進行分配,如1000m的堆Eden是800m,二個Survivor各佔100m,那它們是如何執行的呢?
- 始終會有一塊Survivor是空著的,記憶體使用率是90%
- 程式執行會在Eden和其中一塊Survivor 1中分配記憶體
- 等到執行Minor gc,會將存活下來的物件移動到空著的Survivor 2中
- 然後在Eden和Survivor 2中繼續分配記憶體,Survivor 1空著等著下次使用
這樣就能使記憶體使用率達到90%,也不會產生記憶體碎片。
老年代回收
老年代物件即使進行了垃圾回收,物件的存活率也高,所以採用標記清除或標記整理演算法都是不錯的選擇,這裡就不做闡述。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對我們的支援。