JVM核心神技:GC的內存分配

學習導圖

JVM核心神技:GC的內存分配


一.為什麼要學習GC&內存分配?

時代發展到現在,如今的內存動態分配與內存回收技術已經相當成熟,一切看似進入了“自動化”時代,不免發出疑問:"為啥我們還要了解垃圾收集和內存分配?"

答案很簡單,當需要排查各種內存溢出/洩漏問題的時候,當垃圾收集成為系統達到更高併發量的瓶頸的時候,我們必須對"自動化"技術進行必要的監控和調節。

所以,我們要了解下GC&內存分配,為工作中或者是面試中實際的需要打好基礎。

二.核心知識點歸納

2.1 對象存活判定算法

在瞭解對象存活的判定之前,我們先來了解下四種引用類型

  • 強引用StrongReference

具有強引用的對象不會被GC即便內存空間不足,JVM寧願拋出OutOfMemoryError使程序異常終止,也不會隨意回收具有強引用的對象

  • 軟引用SoftReference

只具有軟引用的對象,會在內存空間不足的時候被GC,如果回收之後內存仍不足,才會拋出OOM異常軟引用常用於描述有用但並非必需的對象,比如實現內存敏感的高速緩存

  • 弱引用WeakReference

只被弱引用關聯的對象,無論當前內存是否足夠都會被GC強度比軟引用更弱,常用於描述非必需對象

  • 虛引用PhantomReference

僅持有虛引用的對象,在任何時候都可能被GC(和弱引用一樣)主要作用是為了垃圾收集器回收時收到一個系統通知(PhantomRefernece類實現虛引用)與弱引用的區別:不同之處在於弱引用的get方法,虛引用的get方法始終返回null,

弱引用可以使用ReferenceQueue,虛引用必須配合ReferenceQueue使用必須和引用隊列 (ReferenceQueue)聯合使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列

2.1.1 引用計數算法

定義:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的

然而在主流的Java虛擬機裡未選用引用計數算法來管理內存,主要原因是它難以解決對象之間相互循環引用的問題,所以出現了另一種對象存活判定算法

<code>//相互循環引用的DEMO
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;

System.gc();
}
}/<code>

2.1.2 可達性分析法

定義:通過一系列被稱為『GC Roots』的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的

可作為GC Roots的對象:

虛擬機棧中引用的對象,主要是指棧幀中的本地變量表本地方法棧中Native方法引用的對象方法區中類靜態屬性引用的對象方法區中常量引用的對象JVM內部的引用(基本數據類型對應的Class對象)所有被同步鎖(synchronized關鍵字)持有的對象反映JVM內部情況的JMXBean、JVMTI中的註冊的回調、本地代碼緩存等

JVM核心神技:GC的內存分配

Q:可達性分析算法中被判定不可達的對象真的被判『死刑』了嗎?

A:在可達性分析算法中被判定不可達的對象還未真的判『死刑』,一共至少要經歷兩次標記過程:

  • 第一次標記:當第一次可達性分析後沒有與GC Roots相連接的引用鏈,被第一次標記
  • 第二次標記:

判斷對象是否有必要執行finalize()方法;若被判定為有必要執行finalize()方法,之後還會對對象再進行一次篩選,如果對象能在finalize()中重新與引用鏈上的任何一個對象建立關聯,將被移除出“即將回收”的集合。

JVM核心神技:GC的內存分配

引申:有關方法區的GC,可分成兩部分

廢棄常量與回收Java堆中的對象的GC很類似,即在任何地方都未被引用的常量會被GC。無用的類

需滿足以下三個條件才會被GC:

A.該類所有的實例都已被回收,即Java堆中不存在該類的任何實例;

B.加載該類的ClassLoader已經被回收;

C.該類對應的java.lang.Class對象沒在任何地方被引用,即無法在任何地方通過反射訪問該類的方法。

2.2 垃圾收集算法

前文講了JVM會回收哪些對象,下文筆者將探究JVM如何回收這些對象

2.2.1 分代收集理論

Q1:三個假說是什麼?

  • 弱分代假說:絕大多數對象都是朝生夕滅
  • 強分代假說:熬過越多次垃圾收集過程的對象就難以消亡
  • 跨代引用假說:跨代引用相對於同代來說僅佔極少數(存在引用關係的對象應該傾向於同時生存或者同時消亡的,例如某個新生代被老年代所引用,該引用會使新生代對象在收集時同樣存活,進而進入老年代)

在新生代上建立一個全局的數據結構(記憶集),將老年代劃分成若干小塊,標識出老年代哪一塊內存存在跨代引用,Minor GC時,在跨代引用的內存裡的對象才會加入到GC Roots進行掃描

Q2:垃圾收集器一致的設計原則

  • 收集器應將Java堆劃分出不同的區域,然後將回收對象依據其年齡(年齡是對象熬過垃圾收集過程的次數)分配到不同的區域之中儲存
  • 如果一個區域中大多數對象都是朝生夕滅,將他們集中到一起,每次回收時只關注少量存活,能以較低代價回收到大量的空間
  • 如果是難以消亡的對象,把他們集中放在一起,虛擬機用較低頻率來回收這個區域,同時兼顧垃圾收集的時間開銷和內存的空間

Q3:如何根據各個年代的特點選擇算法呢?

  • 新生代:大批對象死去,只有少量存活。使用『複製算法』,只需複製少量存活對象即可
  • 老年代:對象存活率高。使用『標記—清理算法』或者『標記—整理算法』,只需標記較少的回收對象即可

這三種算法,筆者將在下文為您詳細解析

2.2.2 複製算法

  • 定義:把可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用盡後,把還存活著的對象『複製』到另外一塊上面,再將這一塊內存空間一次清理掉
  • 優點:每次都是對整個半區進行內存回收,無需考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效
  • 缺點:每次可使用的內存縮小為原來的一半,內存使用率低
JVM核心神技:GC的內存分配

  • Appel式回收

分為一塊較大的Eden空間和兩塊較小的Survivor空間,在HotSpot虛擬機中默認比例為8:1:1。每次使用Eden和一塊Survivor,回收時將這兩塊中存活著的對象一次性地複製到另外一塊Survivor上,再做清理。可見只有10%的內存會被“浪費”,倘若Survivor空間不足還需要依賴其他內存(老年代)進行分配擔保

2.2.3 標記-清除算法

  • 首先『標記』出所有需要回收的對象,然後統一『清除』所有被標記的對象
  • 是最基礎的收集算法
  • 缺點:

『標記』和『清除』過程的效率不高空間碎片太多。『標記』『清除』之後會產生大量不連續的內存碎片,可能會導致後續需要分配較大對象時,因無法找到足夠的連續內存而提前觸發另一次GC,影響系統性能

JVM核心神技:GC的內存分配

2.2.4 標記-整理算法

  • 首先『標記』出所有需要回收的對象,然後進行『整理』,使得存活的對象都向一端移動,最後直接清理掉端邊界以外的內存
  • 優點:即沒有浪費50%的空間,又不存在空間碎片問題,性價比較高
  • 缺點:移動在老年代每次回收都存在大量對象存活區域,必須暫停用戶應用程序才能進行(Stop The World)
  • 一般情況下,老年代會選擇標記-整理算法。
JVM核心神技:GC的內存分配

2.2.5 和稀泥式

解決方法:大部分時間使用標記-清除算法,當內存空間的碎片程度影響到內存分配,再使用標記-整理算法進行收集

2.3 HotSpot算法實現&垃圾回收器

接下來介紹如何在HotSpot虛擬機上實現對象存活判定算法和垃圾收集算法,並保證虛擬機高效運行

2.3.1 枚舉根節點

主流JVM使用的都是準確式GC,在執行系統停頓之後無需檢查所有執行上下文和全局的引用位置,而是通過一些辦法直接獲取到存放對象引用的地方,在HotSpot中是通過一組稱為OopMap的數據結構來實現的,完成類加載後會計算出對象某偏移量上某類型數據、JIT編譯時會在特定的位置記錄棧和寄存器中是引用的位置。這樣GC在掃描時就可直接得知這些信息,並快速準確地完成GC Roots的枚舉

2.3.2 安全點

上述“特定的位置”被稱為安全點,即程序執行時並非在所有地方都停頓執行GC,只在到達安全點時才暫停,降低GC的空間成本

  • 安全點的選定標準:可讓程序長時間執行的地方,如方法調用、循環跳轉、異常跳轉等具有指令序列複用的特徵
  • 使所有線程在最近的安全點上再停頓的方案:

搶先式中斷:無需代碼主動配合,在GC發生時把所有線程全部中斷,若線程中斷處不在安全點上就恢復線程,讓它“跑”到安全點上。現在幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件主動式中斷:在GC要中斷線程時不直接對線程操作,而是設置一箇中斷標誌,讓各個線程在執行時主動輪詢它,當中斷標誌為真時就自己中斷掛起

2.3.3 安全區域

安全點機制只能保證程序執行時,在不太長的時間內遇到可進入GC的安全點,但在程序不執行時(如線程處於Sleep或Blocked狀態)線程無法響應JVM的中斷請求,此時就需要安全區域來解決

  • 安全區域:
    引用關係不會發生變化的一段代碼片段,在安全區域中的任意地方開始GC都是安全的(因為引用關係不變),可看做是擴展的安全點
  • 執行過程:當線程執行到安全區域中的代碼時就標識一下,如果這時JVM要發起GC就不用管被標識的線程;在線程要離開安全區域時檢查系統是否已經完成了根節點枚舉,若完成則線程可以繼續執行,否則等待直到收到可以安全離開安全區域的信號為止

2.3.4 JVM中七種回收器

JVM核心神技:GC的內存分配

序號收集器收集範圍算法執行類型1Serial新生代複製單線程2ParNew新生代複製多線程並行3Parallel新生代複製多線程並行4Serial Old老年代標記整理單線程5CMS老年代標記清除多線程併發6Parallel Old老年代標記整理多線程7G1全部複製算法,標記-整理多線程

注意併發和並行的概念:

在GC中:

並行:多條垃圾收集線程並行工作,而用戶線程仍處於等待狀態併發垃圾收集線程與用戶線程一段時間內同時工作(交替執行)

在普通情景中:

並行:多個程序在多個CPU上同時運行,任意一個時刻可以有很多個程序同時運行,互不干擾併發:多個程序在一個CPU上運行,CPU在多個程序之間快速切換,微觀上不是同時運行,任意一個時刻只有一個程序在運行,但宏觀上看起來就像多個程序同時運行一樣,因為CPU切換速度非常快,時間片是64ms(每64ms切換一次,不同的操作系統有不同的時間),人類的反應速度是100ms,你還沒反應過來,CPU已經切換了好幾個程序了

2.4 內存分配和回收策略

對象的內存分配廣義上是指在堆上分配,主要是在新生代的Eden區上,如果啟動了TLAB,將按線程優先在TLAB上分配,少數情況下也可能會分配在老年代中。分配細節還是取決於所使用的GC收集器組合以及虛擬機中與內存相關的參數的設置。以下介紹幾條普遍的內存分配規則

  • 對象優先在Eden分配:大多數情況下對象在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時虛擬機將發起一次Minor GC

新生代GC:發生在新生代的垃圾收集動作。較頻繁、回收速度也較快

  • 老年代GC(Major GC/Full GC):發生在老年代的垃圾收集動作。出現Major GC經常會伴隨至少一次的Minor GC。速度一般比Minor GC慢10倍以上
  • 大對象直接進入老年代:對於需要大量連續內存空間的Java對象(如很長的字符串以及數組),如果大於虛擬機設定的-XX:PretenureSizeThreshold參數值將直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製
  • 長期存活的對象將進入老年代:虛擬機會給每個對象定義一個年齡計數器,當對象在Eden出生並經過第一次Minor GC後仍存活且能被Survivor容納的話,將被移動到Survivor空間中並將對象年齡設為1;當對象在Survivor區中每“熬過”一次Minor GC年齡就+1,直至增加到一定程度(默認為15歲,可通過-XX: MaxTenuringThreshold設置)就會被晉升到老年代中
  • 動態對象年齡判定:為了能更好地適應不同程序的內存狀況,虛擬機並不要求一定要達到-XX: MaxTenuringThreshold設置值才能晉升到老年代,當Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,那麼年齡大於或等於該年齡的對象可以直接進入老年代
  • 空間分配擔保:在發生Minor GC之前虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,若是,說明可確保Minor GC是安全的,反之虛擬機會查看-XX:HandlePromotionFailure設置值是否允許擔保失敗;若允許,會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小;若大於,將嘗試進行一次Minor GC,若小於或者不允許擔保失敗,將改為進行一次Full GC

解釋:當大量對象在MinorGC後仍然存活的情況時,需要藉助老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代,但前提是老年代本身還有容納這些對象的剩餘空間,由於在完成內存回收之前無法預知實際存活對象,只好取之前每次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,從而決定是否進行Full GC來讓老年代騰出更多空間

三.小測試

恭喜你!已經看完了,相信你對JVM GC&內存分配已經有一定深度的瞭解,下面,進行一下小測試,驗證一下自己的學習成果吧!

Q1:垃圾回收算法你瞭解幾種?請你簡要分析一下,並說明其優缺點?

Q2:Java的引用機制有幾種?請簡要分析下,並說明其在Android中的應用場景有哪些?

Q3:安全點你瞭解過嗎?安全區呢?請你介紹下安全區相對安全點的優勢在哪裡?

Q4:怎麼判斷對象是否存活呢?有幾種方法?

關注我,後續更多幹貨奉上!


分享到:


相關文章: