JVM垃圾收集器與內存分配策略

概述

問:垃圾收集器(GC)需要完成的三件事情:

答:1. 哪些內存需要回收?
2. 什麼時候回收?
3. 如何回收?

問:爲什麼我們還要去了解GC和內存分配呢?

答:當需要排查各種內存溢出、內存泄露等問題時,當垃圾收集器成爲系統達到更高併發量的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節;

對象已死嗎

在堆裏面存放的幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事就是確定這些對象中,哪些還“存活”着,哪些已經“死去”;

判斷對象是否“存活”的算法:

  1. 引用計數算法(主流的虛擬機中沒有選用引用計數算法進行內存管理,最主要的原因是它很難解決對象之間相互循環引用的問題):給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失敗時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用。

  2. !!可達性分析算法(主流的商用程序語言的主流實現中):通過一系列的成爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑成爲引用鏈,每當一個對象到GC Roots沒有任何引用鏈相連時(在圖論中,就是從GC Roots到這個對象不可達),則證明此對象不可用。

在Java語言中,可作爲GC Roots的對象包括下面幾種:

1. 虛擬機棧(棧幀中的本地變量表)中引用的對象;
2. 方法區中**類靜態屬性引用**的對象;
3. 方法區中**常量引用**的對象;
4. 本地方法棧中**JNI(即一般說的Native方法)引用**的對象;
  1. 再談引用:JDK1.2以前,Java中的引用的定義很傳統:如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就成這塊內存代表着一個引用;在JDK1.2之後,將引用分爲:強引用(strong reference)、軟引用(Soft reference)、弱引用(Weak reference)、虛引用(Phantom reference);
    1. 強引用:指在程序代碼中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象;
    2. 軟引用:指那些還有用但並非必須的對象;在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收
    3. 弱引用:描述非必須的對象,被弱引用關聯的對象智能生存到下一次垃圾收集器發生之前;當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象;
    4. 虛引用:成爲幽靈引用或幻影引用,一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例;爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被垃圾收集器回收時收到一個系統通知
  2. 生存還是死亡:即使在可達性算法中“不可達”的對象,也並非是“非死不可”,它們暫處於“緩刑”狀態,要真正宣告一個對象死亡,需要經歷兩次標記過程:

    1. 如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法
    2. 如果對象被判定爲有必要執行finalize()方法,那麼這個對象將放置在一個叫做F-Queue的隊列之中,並在稍後由一個虛擬機自動建立的、低優先級的Finalizer線程去執行它;
      • 這裏的執行是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize方法中執行緩慢,或者發生了死循環(更極端的情況),將很有可能會導致F-Queue隊列中其他對象處於永久等待,甚至導致整個內存回收系統崩潰;
      • finalize()方法時對象逃脫死亡的最後一次機會,稍後GC將對F-Queue隊列進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可
      • 任何一個對象的finalize()方法都只會被系統自動調用一次
  3. 回收方法區:在Java虛擬機規範中,可以不要求虛擬機在方法區實現垃圾回收,而且在方法區中實現垃圾回收的“性價比”比較低;

    永生代(方法區)的垃圾收集主要回收兩部分內容

    1. 廢棄常量:回收廢棄常量與回收Java堆中的對象非常類似,如果沒有發生符號引用,並且有必要的話,就會被回收;常量池中的其他類(接口)、方法、字段的符號引用也與此類似;
    2. 無用的類:一個類需要滿足三個條件才能被稱爲無用的類:
      1. 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
      2. 加載該類的ClassLoader已經被回收;
      3. 該類對應的Java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法;
        • 虛擬機可以對滿足上述三個條件的無用類進行回收,這裏說的僅僅是“可以”,而並不是和對象一樣,不使用了就必然會回收;

垃圾收集算法

  1. 標記-清除算法(Mark-Sweep)(最基礎的收集算法,適用於老年代):
    1. 算法分爲標記與清除兩個階段:首先標記所有需要回收的對象,在標記完成後統一回收所有標記的對象;
    2. 該算法有兩個不足之處
      1. 效率問題:標記與清除的效率都不高;
      2. 空間問題:標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作

  1. 複製算法(Copying)(適用於新生代較多的情況,老年代一般不選用這種算法):
    1. 它將可用內存按容量分爲大小相等的兩塊,每次只使用其中的一塊;當這塊內存用完了,就將還存活的對象複製到另外一塊上面,然後再把已使用過得內存空間一次清理掉;
    2. 每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可;但這種算法的代價是將內存縮小爲了原來的一半;
    3. IBM公司研究表明,新生代中的對象98%是“朝生夕死”的,所以不需要按照1:1比例來劃分內存空間,而是將內存劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次只使用Eden和其中一塊Survivor;HotSpot虛擬機默認Eden和Survivor的大小比例是8:1;
    4. 不足之處:在對象存活率較高時,就要進行較多的複製操作,效率就會變低;

  1. 標記-整理算法(Mark—Compact)(適用於老年代):
    1. 標記過程與標記-清除算法過程一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存

  1. 分代收集算法(當前商業虛擬機都採用這種垃圾收集算法):
    1. 根據對象的存活週期的不同將內存分爲幾塊;一般是把Java堆分爲新生代與老年代
    2. 在新生代中,每次垃圾收集時發現有大批對象死去,只有少量存活,就選用複製算法;
    3. 在老年代中,因爲對象的存活率比較高、沒有額外的空間對它進行分配擔保,就必須使用“標記-清除”算法或者“標記-整理”算法來進行垃圾回收;

HotSpot的算法實現

  1. 枚舉根節點(GC Roots):
    1. 可以作爲GC Roots的節點主要在全局性引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中;
    2. 可達性分析對執行時間的敏感還體現在GC停頓上,因爲這項分析工作必須在一個能確保一致性的快照中進行;這裏的“一致性”的意思是指在整個分析期間整個執行系統看起來像凍結在了一個時間點上,不可以出現分析過程中對象引用關係還在不斷變化的情況,該點不滿足的話分析結果的準確性將無法得到保證;該點是導致GC進行時必須停頓所有Java執行線程的其中一個重要原因,枚舉根節點時都必須要停頓
    3. 在HotSpot虛擬機中,使用一組稱爲OopMap的數據結構來使虛擬機知道哪些對象存放着對象引用;在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用
  2. 安全點(Safepoint):
    1. 在OopMap的協助下,HotSpot可以快速且準地完成GC Roots枚舉;
    2. HotSpot**沒有爲每條指令都生成OopMap**,只是在“特定位置”記錄了這些信息,這些位置稱爲安全點(Safepoint),即程序執行時並非在所有地方都能停頓下來開始GC,只有達到安全點時才能暫停;Safepoint的選定不能太少以致於讓GC等待時間太長,也不能過於頻繁以致於過分增大運行時的負荷;
    3. 安全點的選定基本上是以程序“是否具有讓程序長時間執行的特徵”爲標準進行選定的————因爲每條指令執行的時間都非常短暫,程序不太可能因爲指令流長度太長這個原因而過長時間運行,“長時間執行”的最明顯特徵就是指令序列複用(例如方法調用、循環跳轉、異常跳轉等),所以具有這些功能才能產生Safepoint;
    4. 對於Safepoint,如何讓所有線程在都“跑”到最近的安全點上停頓下來;
      1. 兩種方法:
        1. 搶先式中斷(現在幾乎沒有虛擬機採用這種中斷方式來暫停線程):不需要線程的執行代碼去配合,在GC發生時,首先把所有線程全部中斷,如果發現線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上;
        2. 主動式中斷:當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起輪詢標誌的地方和安全點時重合的,另外在加上創建對象需要分配內存的地方;
  3. 安全區域(Safe Region):在安全區域任意地方開始GC都是安全的;內存回收如何進行是由虛擬機所採用的GC收集器來決定的,而通常虛擬機往往不止有一種GC收集器

垃圾收集器

  1. Serial收集器:
    1. 用於新生代收集;
    2. 是一個單線程的收集器;它的“單線程”的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作;
    3. 在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束
    4. 用戶線程的停頓時間在不斷縮短,但是仍然沒有辦法完全消除
    5. 依然是虛擬機運行在Client模式下的默認新生代收集器;
    6. 優於其他收集器的地方:簡單而高效(與其他收集器的單線程相比),對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷專心做垃圾收集自然可以獲得最高的單線程收集效率;

  1. ParNew收集器:
    1. 其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其餘行爲與Serial收集器的完全一樣(所有控制參數、收集算法、Stop The World、對象分配規則、回收策略等);
    2. 運行在Server模式下的虛擬機中首選的新生代收集器
    3. 除了Serial收集器外,目前只有它能與CMS收集器配合一起工作;
    4. 單CPU的環境中絕對不會比Serial收集器有更好的效果,由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分百超越Serial收集器;
    5. 默認開啓的收集線程數與CPU的數量一樣多

  1. 並行與併發 的概念

    1. 並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程依然處於等待狀態
    2. 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行執行,可能交替執行),用戶線程在繼續運行,而垃圾收集線程在另一個CPU上運行
  2. Parallel Scavenge 收集器(無法與CMS收集器配合工作)(吞吐量):

    1. 是一個新生代收集器,也是使用複製算法的收集器,又是並行的多線程收集器
    2. 它的特點是它的關注點與其他收集器不同,它的目標是達到一個可控制的吞吐量;所謂吞吐量就是CPU用於用戶運行代碼的時間與CPU總消耗的時間的比值(即吞吐量=運行用戶代碼的時間/(運行代碼的時間+垃圾收集的時間));高吞吐量可以高效地利用CPU的時間儘快完成任務,主要適應於後臺運算且不需要太多交互的任務;
    3. GC的停頓時間縮短是以犧牲吞吐量和新生代空間來換取的;停頓時間在下降,吞吐量也在下降;
    4. 由於與吞吐量有關係,因此Parallel Scavenge也被稱爲“吞吐量優先”收集器;
    5. 虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大吞吐量,這種調節方式稱爲GC自適應的調節策略

  1. Serial Old收集器:
    1. Serial收集器的老年代版本,是一個單線程收集器,使用“標記-整理”算法;
    2. 主要意義在於給Client模式下的虛擬機使用;
    3. 如果在Server模式下,它主要有兩大用途:
      1. 在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用;
      2. 作爲CMS收集器的後備預案;

  1. Parallel Old收集器(吞吐量):
    1. Parallel Scavenge收集器的老年代版本,使用多線程“標記-整理”算法;
    2. 注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器;

  1. CMS收集器(Concurrent Mark Sweep)(低停頓):
    1. 是一種以獲取最短回收停頓時間爲目標的收集器;
    2. 基於“標記-清除”算法的收集器;在運作過程中,有四個步驟(初始標記與重新標記仍然需要“stop the world”):
      1. 初始標記(CMS initial mark):僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快;
      2. 併發標記(CMS concurrent mark):進行GC Roots Tracing過程;
      3. 重新標記(CMS remark):爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這一階段的停頓時間比初始標記要長一點,但遠比並發標記時間更短;
      4. 併發清除(CMS concurrent sweep);
    3. 由於整個耗時最長的併發標記併發清除過程收集器線程是與用戶線程一起工作的,所以總體上說,CMS收集器的內存回收過程是與用戶線程一起併發執行的;
    4. 優點:併發收集,低停頓;又被稱爲“併發低停頓收集器”;
    5. 缺點:
      1. CMS收集器對CPU資源特別敏感:CMS默認啓動的回收線程數是(CPU數量+3)/4;(後來爲了解決CPU資源的問題,提供了一種“增量式併發收集器”的CMS收集器的變種,就是在併發標記、併發清除的時候讓GC線程與用戶線程併發執行,使GC線程的獨佔資源時間減少,對整個垃圾收集的過程會更長,但對用戶程序的影響會顯得少一些,但後來發現該收集器效果一般,在目前版本中,不在推薦使用了);
      2. CMS收集器無法處理浮動垃圾
        1. 浮動垃圾:由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS**無法在當次收集中處理它們,只好等待下一次GC時再清理;這部分垃圾成爲浮動垃圾**;
      3. CMS收集器是基於“標記-清理”算法的收集器:這意味着在收集完成之後,會有大量空間碎片產生,空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會在老年代有很多空間剩餘,但是無法找到足夠大的連續空間來分配當前的對象,不得不觸發一次Full GC;

  1. G1收集器(低停頓):
    1. 是一款面向服務端應用的垃圾收集器;
    2. G1具備以下特點:
      1. 並行與併發充分利用CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World的停頓時間,部分收集器原本需要停頓Java線程執行的GC動作,而G1收集器仍然可以通過併發的方式讓Java程序繼續執行;
      2. 分代收集:G1收集器可以獨立管理整個GC堆,但它能夠採用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果;
      3. 空間整合:整體上看是基於“標記-整理”算法實現的收集器,從局部上看(兩個Region之間)是基於“複製”算法實現的;但無論如何,這兩種算法都意味着G1收集器在運作期間不會產生大量的內存空間碎片,收集後能提供規整的可用內存;
      4. 可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒;
    3. G1收集器將整個Java堆劃分爲多個大小相等獨立區域(Region),雖然還保留着新生代與老年代的概念,但新生代和老年代不再是物理隔離了,它們都是Region(不需要連續)的集合;
    4. G1收集器之所以能建立可預測的停頓時間模型,是因爲它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集;G1**跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region**(這也就是Garbage-First名稱的由來);
    5. 在G1收集器中,Region之間的對象引用以及其他收集器的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的;
    6. 不計算維護Remembered Set的操作,G1收集器的運作分爲四個步驟:
      1. 初始標記(Initial Marking):僅僅只是標記一下GC Roots**直接關聯到的對象,並且修改TAMS**(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中創建新對象,這階段需要停頓線程,但耗時很短
      2. 併發標記(Concurrent Marking):從GC Roots開始對堆中的對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶線程併發執行
      3. 最終標記(Final Marking):爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分的標記記錄,虛擬機將這段時間對象變化記錄在線程的Remembered Set Logs中,最終標記需要把Remembered Set Logs整合到Remembered Set中,這階段需要停頓線程,但是可並行執行
      4. 篩選回收(Live Data Counting and Evacuation):首先對各個Region的回收價值成本進行排序,根據用戶所期望的GC停頓時間來指定回收計劃,停頓用戶線程將大幅提高收集效率;(但這階段是可以做到與用戶線程併發執行的,時間可由用戶控制的);

理解GC日誌

  1. 每個收集器的日誌格式都不一樣,每一種收集器的日誌格式都是由它們自身的實現所決定的
  2. GC日誌最前面的數字代表了GC發生的時間,這個數字的含義是從Java虛擬機啓動以來經過的秒數;
  3. GC日誌的開頭的[GC和[Full GC說明了這次垃圾收集的停頓類型,如果有“Full”說明這次GC是發生了Stop-The-World的;如果調用了System.gc()方法所觸發的收集,那麼在這裏講顯示“[Full GC(System)”;
  4. GC日誌的[DefNew、[Tenured、[Perm表示GC發生的區域,這裏顯示的區域名稱與GC收集器是密切相關的;例如Serial收集器中的新生代名爲“Default New Generation”,所以顯示“[DefNew”;
  5. 方括號內部的“3224K->152K(3712K)”的含義是“GC前內存區域已使用的容量->GC後內存區域已使用的容量(該內存區域的總容量)”;
  6. 方括號外部的“3324K->152K(11904K)”表示“GC前Java堆已使用的容量->GC後Java堆已使用的容量(Java堆總容量)”;
  7. GC日誌裏面的user、sys、real與Linux的time命令所輸出的時間含義一致,分別代表用戶態消耗的CPU時間內核態消耗的CPU時間操作從開始到結束所經過的牆鍾時間(牆鍾時間包括各種非運算的等待耗時,而CPU時間不包括這些耗時,但當系統有多個CPU或者多核的話,多線程操作會疊加這些CPU時間)

內存分配與回收策略

  1. 自動內存管理:給對象分配內存、回收分配給對象的內存;
  2. 默認環境:Client模式虛擬機,使用Serial/Serial Old收集器下;

對象優先在Eden分配

  1. 對象在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC;
  2. Minor GC與Full GC的區別:
    1. 新生代GC(Minor GC):發生在新生代的GC,因爲Java對象大多數都具備朝生夕死的特性,所以Minor GC**非常頻繁,一般回收速度比較快**;
    2. 老年代GC(Major GC/Full GC):發生在老年代的GC,出現了Major GC,經常會伴隨至少一次Minor GC(但非絕對的,在PS收集器的收集策略裏就有直接進行Major GC的策略選擇過程),Major GC的速度一般比Minor GC慢10倍以上

大對象直接進入老年代

  1. 所謂的大對象是指需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組
  2. 經常出現大對象容易導致內存還有不少內存空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們;

長期存活的對象將進入老年代

  1. 虛擬機給每個對象定義了一個對象年齡(Age)計數器;如果對象在Eden出生並經過第一Minor GC後仍然能存活着,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象的年齡設爲1;對象在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度時(默認爲15歲),將會被晉升爲老年代中;

動態對象年齡判定

  1. 如果在Survivor空間相同年齡所有對象大小的總和*大於Survivor空間的一半,年齡大於或者等於該年齡的對象就可以直接進入老年代*;

空間分配擔保

  1. 在發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代所有對象的總空間,如果這個條件成立,那麼Minor GC是安全的;如果這個條件不成立,虛擬機會去查看是否允許擔保失敗
  2. 如果允許擔保失敗,那麼會繼續檢查老年代最大的可用連續空間是否大於歷次晉升到老年代的對象的平均大小,如果大於,將嘗試一次Minor GC,但是這次Minor GC是有風險的;如果小於,這時將進行一次Full GC;
  3. 如果大量對象在Minor GC後仍然存活,就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代中,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成內存回收之前是無法知道的,所以只好取每一次晉升到老年代對象容量的平均大小值作爲經驗值,與老年代剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多的空間;
  4. JDK1.6Update24之後的規則變爲:只要老年代的連續空間大於新生代對象的總大小或者大於歷次晉升到老年代的平均大小就會進行Minor GC,否則就進行Full GC;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章