1. 程式人生 > 其它 >JVM的GC機制

JVM的GC機制

JVM的GC機制

1. 什麼物件會被回收

  • 引用計數法:如果一個物件被引用一次,則記錄引用次數加一,如果引用取消,則減一,當減到0時,需要被回收。

    問題:迴圈引用,A引用B,B引用A,除此之外,已經無法訪問他們。

  • 可達性分析演算法:從GC根開始,找到GC根直接或間接引用的物件並標記,沒有標記的便是需要回收的。

2. 什麼可以作為GC ROOT

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件
  • 本地方法棧中 JNI(即一般說的 Native 方法)引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 同步鎖持有的變數
  • 跨代引用的物件

3. 垃圾收集演算法

3.1 三個假說

  • 大部分物件是朝生夕滅,存活時間很短。
  • 越多次數跨過垃圾回收的物件越難被回收。
  • 跨代引用很少。

3.2 堆區域劃分

  • 新生代:新建立的物件和跨過垃圾回收次數小於某個值的物件在這個區域。
  • 老年代:跨過垃圾回收次數大於某個值的物件在這個區域,預設是15。
  • 有些收集器不使用經典分代,而將記憶體分為等大的Region。

3.3 三個標記演算法

  • 標記-清除演算法:從根開始遍歷,將遍歷到的物件做標記,沒有做標記的被清除。

    問題:清除之後存在記憶體碎片,嚴重影響記憶體利用率。

  • 標記-複製演算法:將記憶體按照比例分開,一部分閒置稱A,另一部分存放物件稱B。一次標記結束後,將存活的物件複製到閒置區域A,將B清空。

    • 大多新生代收集器使用此技術。
    • Appel式回收:將新生代分為一個Eden區和兩個Survior區,比例為8:1,分配記憶體只分配到Eden和一個Survior。HotSpot使用此技術.
    • 逃生門:當Survior放不下Eden中的存活物件,將這些物件直接放到老年代。

    問題:浪費空間、當大量對存活時複製浪費的時間長(故老年代不使用該方法)。

  • 標記-整理演算法:將標記存活著的物件朝記憶體空間的一段移動,清除掉邊界之外的物件。

    問題:需要更新引用,操作複雜。

3.4 重要的演算法

3.4.1 GC Roots列舉

GC Roots大部分存在於棧幀的區域性變量表中,而區域性變量表中可能存放著一些基本資料型別和引用型別,如果要遍歷全部來找出引用型別來作為GC Roots的話,效率過低。

當前主流的虛擬機器都是準確式垃圾收集,也就是說,虛擬機器可以直接知道棧中的變數是基本資料型別還是引用資料型別。

譬如記憶體中有一個32bit的整數123456,虛擬機器可以直接判斷出他是一個整數123456,還是它是指向123456的記憶體地址。

HotSpot是使用OopMap來解決這個問題,在即時編譯的過程中,會在特定的位置記錄下棧裡的哪些地方是引用。在GC Roots列舉的時候,查詢OopMap便能快速找到GC Roots了。

3.4.2 安全點和安全區域

OopMap記錄著引用的資訊,如果每執行一次語句就更新一次OopMap,則會導致效率低下,所以虛擬機器使用了一個叫安全點的技術,安全點中所有執行緒都將掛起,來方便OopMap更新和Gc Roots列舉,在安全點之外不會更新OopMap,OopMap累積到安全點再一次性更新。

如何確定安全點?

虛擬機器以“是否具有讓程式長時間執行的特徵”為標準選擇安全點,準確的說就是:1. 方法呼叫 2. 迴圈跳轉 3. 異常跳轉。

如何讓所有執行緒都跑到安全點掛起?
  • 搶先式中斷:

    虛擬機器要垃圾回收的時候將全部執行緒中斷,如果有執行緒不在安全點上,則讓這個執行緒執行,讓他也走到安全點。(如果這個時間例項化了個超大物件怎麼辦?)

  • 主動式中斷:

    虛擬機器不會主動中斷執行緒,而是設定一個標誌位,所有執行緒執行過程中不斷輪詢這個標誌位,如果這個標誌為真就在最近的安全點掛起,標誌位和安全點是重合的,並且加上所有要建立物件和分配記憶體的地方(好像能解決上面這個問題了)。

安全區域

有些執行緒Sleep,他們不能走到安全點掛起,其他執行緒也不能等他們醒來,所以設定安全區域,在安全區域內所有引用關係將不會變化,安全區域就是拉長的安全點。

3.4.3 記憶集和卡表

上面提到GC Roots包含著跨代引用的物件,如果要蒐集新生代,如何找到跨代引用的物件,莫非要遍歷整個老年代?這個問題通過記憶集的技術來解決。

記憶集記錄著從非收集區指向收集區域的指標的集合的資料結構。在垃圾回收的時候,便能通過記憶集來找出可以作為GC Root的跨代引用的物件。

卡表:

記憶集有精讀之分,有些能精確到具體地址,有些只能精確到一塊記憶體區域,HotSpot就是精確到一塊記憶體區域,這種記憶集稱為卡表。HotSpot的卡表是個陣列,陣列中的每個元素對應一個記憶體塊,稱為卡頁,如果一個卡頁的值是1,則說明該記憶體塊中包含著跨代指標,將他們加入GC Roots中一起掃描。

3.4.4 寫屏障

寫屏障可以解決何時更新卡表的問題,寫屏障簡單而言是個AOP切面,當更新了引用時,會通過寫屏障自動更新卡表資訊。

3.4.5 三色標記

GC Roots的列舉會暫停所有執行緒,而現在的許多收集器在標記過程中是不需要暫停執行緒的,可是併發標記會帶來漏標和錯標,一旦錯標,將會導致程式正在使用物件被回收,導致程式崩潰,為解決此問題,引入三色標記來解決。

  • 黑色:該物件已經被標記過了,且該物件下的屬性也全部都被標記過了。(程式所需要的物件)
  • 灰色:該物件已經被標記過了,但該物件下的屬性沒有全被標記完。(GC需要從此物件中去尋找垃圾)
  • 白色:該物件沒有被標記過。(物件垃圾)
模擬漏標

當掃描結束後,所有非垃圾節點都變成了黑色,這時如果某個引用取消,則被引用的成垃圾,可仍然是黑色,屬於漏標。

判定錯標的兩個條件

賦值器插入了一條或者多條從黑色物件到白色物件的新引用。

賦值器刪除了全部從灰色物件到該白色物件的直接或間接引用。

  • 為何需要條件1?

    • 如果不是插入黑色到白色而是插入灰色到白色,這樣下一輪掃描就會掃描灰色,必定會把新插入的白色物件也標記上。
    • 如果不是插入黑色到白色而是插入白色1到白色2,分兩種情況:
      1. 假設白色1不是垃圾,則它遲早會被標記,那麼白色2也會被標記。
      2. 假設白色1是垃圾,那如何找到白色1?假設不存在。
  • 為何需要條件2?

    • 如果不是刪除灰色到白色,而是刪除黑色到白色,此假設不存在,黑色後面都是灰色。
    • 如果不是刪除灰色到白色,而是刪除白色到白色,分兩種情況:
      1. 假設白色1不是垃圾,那麼所有灰色物件必會有一個間接或直接引用他。
      2. 假設白色1是垃圾,那如何找到白色1?假設不存在。
解決錯標

增量更新:為了打破條件1。當賦值器插入了一條或者多條從黑色物件到白色物件的新引用時,將黑色物件變成灰色。

原始快照:為了打破條件2。賦值器刪除了全部從灰色物件到該白色物件的直接或間接引用時,將改變前的引用關係快照儲存,待併發掃描結束後,在掃描一遍改變前的快照(如果只滿足了條件2,不滿足條件1,這樣做是不是會有可能產生浮動垃圾)。

4. 一些經典的垃圾收集器

4.1 CMS收集器

用於老年代的收集器。
運作過程

  1. 初始標記
  2. 併發標記
  3. 重新標記
  4. 併發清除

過程詳解

  1. 初始標記:GC Roots列舉,需要停頓所有執行緒,由於OopMap,速度很快。
  2. 併發標記:從GC Roots開始,遍歷所有關聯到的物件,無需停頓,於使用者執行緒併發執行,使用三色標記演算法,對於引用關係的改變,採取增量更新的方法解決。
  3. 重新標記:將修正增量更新的改變修正。
  4. 併發清除:清除垃圾。

缺點

  1. 併發標記是和使用者執行緒一起執行,會佔用處理器,導致應用程式變慢。
  2. 使用的是標記-清除演算法,會產生記憶體碎片。
  3. 會產生浮動垃圾,浮動垃圾過多,將導致Full GC的出現。

4.2 G1收集器

G1收集器面向整個堆記憶體進行回收,衡量標準不是分代,而是將堆記憶體分為等大的Region,哪個Region的回收價值高,回收那個Region。

5. 垃圾回收的時機

  1. Eden區滿了,會進行一次新生代的收集。
  2. 新生代垃圾回收前,判斷老年代的連續空間 < Eden每次收集後存活物件的平均值,進行老年代收集。
  3. 使用CMS收集器時,老年代的空間被佔用了92%,進行老年代收集。
  4. 新生代垃圾回收後,存活的物件 > 老年代空間,進行老年代收集。

6. 記憶體分配策略

  1. 新建立的物件分配在Eden區。
  2. 新建立的大物件直接分配在老年代(所以要避免建立短命大物件)。
  3. 長期存活的物件進入老年代。
  4. 新生代收集後,Survior放不下,直接進入老年代(所以要適當調整Eden和Survior的比例,來確保朝生夕滅的物件每次收集都能在Survior中放下)。
  5. 動態年齡判定,相同年齡的物件所佔用的Survior空間大於Survior的一半,所有等於或大於這個年齡的物件都進入老年代。