1. 程式人生 > >深入理解Java虛擬機器讀書筆記2----垃圾收集器與記憶體分配策略

深入理解Java虛擬機器讀書筆記2----垃圾收集器與記憶體分配策略

二 垃圾收集器與記憶體分配策略 1 JVM中哪些記憶體需要回收?     JVM垃圾回收主要關注的是Java堆和方法區這兩個區域;而程式計數器、虛擬機器棧、本地方法棧這3個區域隨執行緒而生,隨執行緒而滅,隨著方法結束或者執行緒結束記憶體自然跟隨著回收了,因此不需要過多考慮記憶體分配和回收的問題。   2 判斷物件是否存活的演算法     (1)引用計數演算法             基本思路:給物件新增一個引用計數器,每當有一個地方引用它,計數器值加1;當引用失效時,計數器減1;任何時刻計數器為0的物件就是不可能再被使用的。             優點:實現簡單,判定效率高。             缺點:很難解決物件之間迴圈引用的問題。             應用:Python語言等。     (2)可達性分析演算法             基本思路:通過一系列的稱為“GC Roots"的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。             Java語言中,GC Roots的選取為:                 · 虛擬機器棧中引用的物件;                 · 方法區中類靜態屬性引用的物件;                 · 方法區中常量引用的物件;                 · 本地方法中JNI引用的物件。             應用:Java、C#等。  
3 JVM中如何判斷一個物件已經死亡?     判定一個物件死亡要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法(當該物件沒有覆蓋finalize()方法或者finalize()方法已經被呼叫過,虛擬機器將這兩種情況都視為”沒有必要執行“)。若這個物件被判定為有必要執行finalize()方法,那麼這個物件將會被放置在一個叫做F-Queue的佇列中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果該物件在執行finalize()後重新與引用鏈上的任何一個物件建立了關聯,那麼在第二次標記時它將被移除出”即將回收“的集合;否則,該物件就將被回收。  
4 強引用、軟引用、弱引用、虛引用的區別     強引用:程式程式碼中普遍存在,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。     軟引用:描述一些還有用但非必需的物件,若一個物件具有軟引用,則記憶體空間足夠時,垃圾收集器不會回收它,記憶體空間不足時,就會回收該物件。可用來實現記憶體敏感的快取記憶體。     弱引用:強度比軟引用更弱,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。     虛引用:也稱為幽靈引用或者幻影引用,一個物件是否有弱引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的是在這個物件被收集器回收時收到一個系統通知。      軟引用、弱引用、虛引用都可以和引用佇列聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有軟引用(或弱引用、虛引用),就會在回收物件的記憶體之前,把這個軟引用(或弱引用、虛引用)加入到與之關聯的引用佇列中。  
5 方法區的回收     回收內容:廢棄常量和無用的類。     廢棄常量的判定:沒有任何引用指向該常量。     無用的類的判定:         · 該類的所有例項都已經被回收,也即Java堆中不存在該類的任何例項;         · 載入該類的ClassLoader已經被回收;         · 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。     方法區回收的”價效比“一般比較低,虛擬機器可以實現成不進行方法區的垃圾收集。   6 垃圾回收演算法及應用場景     (1)標記-清除演算法             過程:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件;             不足:                 · 效率問題。標記和清除兩個過程的效率都不高;                 · 空間問題。標記清除後會產生大量的不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。             應用:CMS收集器。     (2)複製演算法             過程:將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面。然後再把已使用過的記憶體空間一次性清理掉。             優點:實現簡單,執行高效,而且不用考慮記憶體碎片問題。             缺點:代價是將記憶體縮小為原來的一半。             改進:將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的物件一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。             改進版本缺點:每次回收時存活的物件可能比較多,導致Survivor空間不夠用,需要依賴其他記憶體(老年代)進行分配擔保。             應用:新生代垃圾收集器,如Serial、ParNew、Parallel Scavenge以及G1收集器的區域性上看。     (3)標記-整理演算法             過程:首先標記出所有需要回收的物件,然後讓存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。             優點:不會出現空間碎片問題。             應用:老年代垃圾收集器,如Serial Old、Parallel Old、G1收集器的整體上看。     (4)分代收集演算法             過程:根據物件存活週期的不同將記憶體劃分成幾塊,一般是將Java堆分為新生代和老年代,然後根據各個年代的特點採用最適當的收集演算法。             使用:一般新生代使用複製演算法,老年代使用標記-清除或標記-整理演算法。   7 HotSpot的演算法實現     (1)列舉根節點             使用一組稱為OopMap的資料結構來達到使虛擬機器知道哪些地方存放著物件引用的目的。     (2)安全點             程式執行時並非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。             安全點的選擇:以”是否具有讓程式長時間執行的特徵“為標準進行選定的。             ”長時間執行“的特徵:指令序列複用,如方法呼叫、迴圈跳轉、異常跳轉等。             如何在GC發生時讓所有執行緒都跑到最近的安全點再停頓下來:                 · 搶先式中斷:在GC發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓它跑到安全點上。                 · 主動式中斷:當GC需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就將自己中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立物件需要分配記憶體的地方。     (3)安全區域             安全區域是指在一段程式碼片段之中,引用關係不會發生變化,在這個區域的任何地方開始GC都是安全的。   8 垃圾收集器     (1)新生代            1)Serial收集器     ---單執行緒:不僅在於只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束(“Stop The World”);     ---使用複製演算法;     ---是虛擬機器執行在Client模式下的預設新生代收集器;     ---可以與CMS收集器配合工作;     ---優點:簡單而高效,對於限定單個CPU的環境下,Serial收集器由於沒有執行緒互動的開銷,故可獲得最高的單執行緒收集效率。         2)ParNew收集器     ---並行多執行緒:Serial收集器的多執行緒版本;     ---採用複製演算法;     ---暫停所有的使用者執行緒(“Stop The World”);     ---執行在Server模式下的虛擬機器中首選的新生代收集器;     ---可以與CMS收集器配合工作。         3)Parallel Scavenge收集器     ---並行多執行緒;     ---使用複製演算法;     ---“吞吐量優先”收集器,目標是達到一個可控制的吞吐量(CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值),其他收集器的關注點是儘可能縮短垃圾收集時使用者執行緒的停頓時間;     ---具有自適應調節策略:虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整引數以提供最合適的停頓時間或者最大的吞吐量。     (2)老年代         1)Serial Old收集器     ---單執行緒;     ---使用標記-整理演算法;     ---主要用於給Client模式下的虛擬機器使用;     ---在Server模式下,主要有兩大用途:一是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用;二是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。         2)Parallel Old收集器     ---多執行緒;     ---標記-整理演算法;     ---與Parallel Scavenge收集器配合使用;     ---用於注重吞吐量以及CPU資源敏感的場合。         3)CMS收集器     ---基於標記-清除演算法。     ---目標:獲取最短回收停頓時間。     ---包括四個步驟:     一、初始標記:僅標記GC Roots能直接關聯到的物件,速度快,單執行緒執行;     二、併發標記:進行GC Roots追蹤的過程,速度慢,併發執行;     三、重新標記:修正併發標記期間因使用者執行緒繼續運作而導致產生變動的那一部分物件的標記記錄,速度較快,並行執行;     四、併發清除:速度慢,併發執行;     其中初始標記與重新標記兩步仍需要“Stop The World”。總體來說,其記憶體回收過程是和使用者執行緒一起併發執行的。     ---優點:併發收集、低停頓。     ---缺點:
  1. 對CPU資源非常敏感:在併發階段,因垃圾收集執行緒佔用一部分CPU資源而導致使用者程式變慢,總吞吐量會降低;
  2. 無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生:併發清理階段,隨著使用者執行緒繼續執行,還會有新的垃圾繼續產生,CMS無法在當次收集中處理掉它們,如果執行期間預留的記憶體無法滿足程式的需要,就會出現該失敗,臨時啟用Serial Old收集器重新進行老年代的垃圾收集;
  3. 收集結束時會有大量空間碎片產生。
    (3)G1收集器         ---目標:獲取最短回收停頓時間。         ---面向服務端應用的垃圾收集器。         ---特點:                     · 並行和併發;                     · 分代收集:可以採用不同的方式處理新建立的物件和已經存活一段時間、熬過多次GC的舊物件;                     · 空間整合:整體看基於標記-整理演算法,區域性看基於複製演算法,不會產生記憶體空間碎片;                     · 可預測的停頓:能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。             ---使用G1收集器時,Java堆的記憶體佈局與其他收集器有很大差別,它將Java堆劃分為多個大小相等的獨立區域,新生代和老年代不再是物理隔離的,它們都是一部分Region的集合。             ---G1收集器能建立可預測的停頓時間模型的原因:它避免在整個Java堆中進行全區域的垃圾收集,每次根據允許的收集時間,優先回收價值最大的Region。             ---在G1收集器中Region之間的物件引用和其他收集器中的新生代和老年代之間的物件引用,虛擬機器都是使用Remembered Set來避免全堆掃描的。             ---G1收集器的執行過程:                 · 初始標記:標記GC Roots能直接關聯到的物件,並且修改TAMS的值(以便下一階段使用者程式併發執行時,能在正確可用的Region中建立新物件),單執行緒執行,需要“Stop The World",但速度快,耗時短;                 · 併發標記:從GC Roots開始對堆中物件進行可達性分析,找出存活的物件,併發執行,耗時較長;                 · 最終標記:修正在併發標記期間因使用者程式繼續執行而導致標記產生變動的那一部分標記記錄,並行執行,需要”Stop The World";                 · 篩選回收:首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,並行執行(也可以與使用者執行緒併發執行,但是停頓使用者執行緒將大幅提高收集效率)。   9 記憶體分配策略     ·  物件優先在新生代的Eden分配;     ·  大物件直接進入老年代。大物件:需要大量連續記憶體空間的Java物件,如陣列、字串;     ·  長期存活的物件將進入老年代。虛擬機器給每個物件定義了一個物件年齡計數器;     ·  動態物件年齡判定。如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代;     ·  空間分配擔保。