1. 程式人生 > >JAVA垃圾收集器與記憶體分配策略

JAVA垃圾收集器與記憶體分配策略

3.1 概述 LISP是第一門使用記憶體動態分配和垃圾收集技術的語言。 CG需要完成的三件事: 1、哪些記憶體需要回收? 2、什麼時候回收? 3、如何回收? JAVA堆和方法區中,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也不一樣。我們只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,CG關注的是這部分記憶體。

3.2 物件已死嗎 CG在進行垃圾回收時,最先要確定的,就是這些物件之中,哪些物件死了(即不能被任何途徑使用的物件),哪些還活著。

3.2.1 引用計數演算法 很多教科書的辦法,是給物件新增一個計數器,每當有個地方引用它,就加一,當引用失效,就減一。任何時刻計數器為0的物件就不可能被使用。客觀地說,這個方法實現簡單,判定效率高。但是,主流的Java虛擬機器裡面沒有選擇引用計數演算法來管理記憶體。最主要的原因是,它很難解決物件之間相互迴圈引用的問題。

3.2.2 可達性分析演算法 在主流的商用程式語言中(Java ,C#,甚至是Lisp),都是通過可達性分析(Reachability Analysis)來判斷物件是否存活。這個演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為“引用鏈”(Reference Chain)。當一個物件到GC Root沒有任何引用鏈相連(即從GC Root 到該物件不可達)則證明這個物件是不可用的。 在Java語言中,可作為GC Root 的物件包括以下幾種。 1—》虛擬機器棧(棧幀中的本地變量表)中引用的物件。 2—》方法區中類靜態屬性引用的物件。 3—》方法區中常量引用的物件。 4—》本地方法棧中JNI(即一般所說的Native方法)引用的物件。

3.2.3 再談引用 JDK1.2之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference),軟引用(Soft Reference),弱引用(Weak Reference ),虛引用(Phantom Reference) 4種。這四種強度逐漸減弱。 強引用—》在程式碼中普遍存在的,類似“Object obj = new Object() ”這類的引用。只要強引用還 在,垃圾收集器永遠不會回收掉被引用的物件。 軟引用—》描述一些還有用,但是並非必需的物件。對於軟引用關聯著的物件,在系統將要發生 記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行二次回收。如果這次回收 還沒有足夠的記憶體,才會丟擲記憶體溢位異常。提供了SoftReference類來實現軟引用。 弱引用—》也是用來描述非必需物件的。但是強度低於軟引用,被弱引用關聯的物件只能生存到 下次垃圾收集發生之前。當垃圾收集器工作室,無論當前記憶體是否足夠,都會回收掉 只被弱引用關聯的物件。提供了WeakReference類來實現弱引用。 虛引用—》也稱為幽靈引用或者幻影引用。一個物件是否有虛引用的存在,不會對其生存時間構 成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一 目的是在這個物件被收集器回收時收到一個系統通知。提供了PhantomReference類 來實現虛引用。虛引用必須和引用佇列一起使用。

3.2.4 生存還是死亡 即使在可達性分析演算法中不可達的物件,也不是非死不可。他們暫時處於“緩刑階段”,要真正宣告一個 物件死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GC Roots相連的引用鏈,將會被第一次標記,並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”。 如果這個物件被判斷 有必要執行finalize()方法,那麼這個物件將會放置在一個叫做F-Queue的佇列中,並在稍後有一個由虛擬機器自己建立的,低優先順序的Finalizer執行緒去執行它。這裡所謂的“執行”是指虛擬機器會觸發這個方法,但並不承諾會等待他執行結束,這樣做的原因是,如果一個物件在finalize方法中執行緩慢,或者發生了死迴圈,將可能會導致F-Queue佇列中其他物件永久處於等待狀態,導致整個記憶體回收系統崩潰。Finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果對想要在Finalize()中成功拯救自己,只要重新與引用鏈上的任何一個物件建立關聯即可。注意!!任何一個物件的Finalize()方法都只會被系統自動呼叫一次。

3.2.5 回收方法區 很多人認為方法區(永久代)是沒有垃圾收集的。Java虛擬機器規範中的確說過可以不要求虛擬機器在方法區實現垃圾收集,但是永久代中的確有可回收的內容。 內容主要有兩部分: 1、廢棄常量 判定標準:假如一個字串“abc”已經進入了常量池,但是當前系統沒有任何一個String 物件引用常量池中的“abc”常量。如果這時發生記憶體回收,而且必要的話,這個“abc”常 量就會被系統清理出常量池。其他類,介面,方法,欄位的符號引用也與此類似。 2、無用的類 同時滿足以下三個條件: (1)該類的所有例項都已經被回收。 (2)載入該類的ClassLoader已經被回收。 (3)該類對應的java.lang.Class物件沒有在任何地方被引用,無法再任何地方通過反射 訪問該類的方法。 虛擬機器可以對滿足上述條件的無用類進行回收,但是並不是和物件一樣,不使用了就必 須回收。是否回收由虛擬機器決定。

3.3 垃圾收集演算法

3.3.1 標記-清除演算法 最基礎的收集演算法是“標記-清除”(Mark-Sweep)演算法,分為標記和清除兩個階段。 首先標記所有需要的物件,在標記完成後統一回收所有被標記的物件。 不足:1-效率問題,,標記和清除兩個過程的效率都不太高。 2-空間問題,,標記清除之後會產生大量不連續的記憶體碎片,可能導致以後 分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾 收集動作。

3.3.2 複製演算法 為了解決效率問題,一種稱為“複製”(Copying)的收集演算法出現了。它將記憶體分為兩塊,每次只用一塊。當這一塊的記憶體用完了,就將還存活的物件複製到另一塊上,再把已使用過的記憶體空間一次清理掉。在實際操作中,不需要按照1:1分配,只需要分配一塊較大的Eden空間和兩塊較小的Survivor空間,每次用Eden和一塊Survivor空間就行。回收時,將Eden和Survivor中存貨的物件複製到另一塊Survivor空間中,在清理掉Eden和剛才用過的Survivor空間。大小比例是8:1:1.

3.3.3 標記-整理演算法 針對老年代的一個適應性改進。標記過程和“標記-清除”演算法類似,但是後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一段移動,然後直接清理掉邊界以外的記憶體。

3.3.4 分代收集演算法 當代商業虛擬機器都使用的分代收集演算法。根據物件存活週期的不同將記憶體劃分為幾塊,一般把JAVA堆分為新生代和老年代,根據各個年代的特點採用最適當的收集演算法。新生代一般使用複製演算法,老年代一般使用標記整理或者標記清理演算法。

3.4 HotSpot的演算法實現 3.4.1 列舉根節點 可達性分析中,能夠作為GC Roots的節點主要在全域性性引用,與執行上下文中。現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這裡面的引用,那麼必然會消耗很多時間。 另外,可達性分析對執行時間的敏感還體現在GC停頓上。因為這項分析工作必須在一個能確保一致性的快照中進行,所以導致GC進行時必須停頓所有JAVA執行執行緒。即使是在號稱幾乎不會發生停頓的CMS收集器中,列舉根節點也是必須要停頓的。 由於目前主流Java虛擬機器使用的都是準確式GC,所以當執行系統停頓下來後,並不需要一個不漏的檢查完所有執行上下文和全域性的引用位置,虛擬機器有辦法直接得知哪些地方存放著物件引用。在HotSpot中使用了一組叫做OopMap的資料結構來達到這個目的的。在類載入完成的時候,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用,這樣,GC在掃描時就可以直接得知這些訊息了。

3.4.2 安全點 在OopMap下,HotSpot可以快速準確的完成GC Roots的列舉,但是OopMap內容變化的指令非常多,如果為每條指令都生成對於的OopMap,將會需要大量的額外空間。 所以,只有在安全點(Safepoint)才記錄了OopMap,即程式執行時並非在所有地方都能停頓下來開始GC,只有到達安全點時才能暫停。 對於安全點,另一個問題是如何在GC發生時讓所有執行緒(不包括進行JNI呼叫的執行緒)都跑到最近的安全點上再停頓下來。這時有兩種方案供選擇: 1–》搶先式中斷(Preemptive Suspension) 不需要執行緒的執行程式碼主動去配合,在 GC發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不在安全 點上,就恢復執行緒,讓它“跑”到安全點上。現在幾乎沒有虛擬機器用此方法。 2–》主動式中斷(Voluntary Suspension) 當GC需要中斷執行緒時,不直接對執行緒進行 操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現 中斷標誌為真時就自己中斷掛起。輪詢標誌的地方和安全點是重合的。 安全點的隱患:Safepoint機制保證了程式執行時,在不太長的時間內就會遇到可進入GC的Safepoint,但是,程式“不執行”的時候呢?所謂程式不執行就是沒有分配CPU時間,例如:執行緒處於Sleep或者Blocked狀態,這時執行緒無法響應JVM的中斷請求,“走”到安全的地方去中斷掛起,JVM也不可能等待執行緒重新被分配CPU時間。此時,就需要安全區域來解決。

3.4.3 安全區域 安全區域是指在一段程式碼片段中,引用關係不會發生變化。在這個區域中的任何地方開始GC都是安全的。我們可以把它當成擴充套件的安全點。

3.5 垃圾收集器 垃圾收集器是記憶體回收的具體體現。虛擬機器內部會有各種不同的垃圾收集器來作用於不同分代的垃圾。他們也可以搭配使用。至今為止沒有一個萬能收集器,只能選擇對具體應用場景最適合的收集器。

3.5.1 Serial 收集器 Serial收集器是最基本,發展歷史最悠久的收集器。曾經是新生代收集的唯一選擇。這是一個單執行緒的收集器,它的單執行緒不僅僅是說明他只使用一個CPU或者一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有工作執行緒,直到它收集結束。 優點:簡單而高效 適用場景:執行在Client模式下的虛擬機器;收集幾十兆或者一兩百兆的新生代,可以控制在幾十毫秒到一百多毫秒以內,可接受。

3.5.2 ParNew收集器 它是Serial收集器的多執行緒版本。使用複製演算法。 優點:能和CMS收集器配合工作。

3.5.3 Parallel Scavenge 收集器 使用複製演算法,並行的多執行緒收集器。 特點:目的是達到一個可控制的吞吐量(吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間))。 適合場景:後臺運算而不需要太多互動的任務。 有自適應的調節策略,能夠提供最合適的停頓時間或者最大的吞吐量。

3.5.4 Serial Old收集器 是Serial收集器的老年代版本。使用“標誌-整理”演算法。 適用場景:執行在Client模式下的虛擬機器。如果是在Server模式下,可作為CMS收集器的後備預案。

3.5.5 Parallel Old收集器 是Parallel Scavenge 收集器的老年代版本。使用多執行緒和“標記-整理”演算法。

3.5.6 CMS收集器(Concurrent Mark Sweep) 是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓的時間段,以帶給使用者較好的體驗。 從名字上看,他是基於“標記-清除”演算法實現的。過程分為4個步驟 1-初始標記(CMS initial mark) 2-併發標記(CMS concurrent mark) 3-重新標記(CMS remark) 4-併發清除(CMS concurrent sweep) 其中,初始標記和重新標記這兩個步驟仍然需要“stop the world”。 初始標記–標記GC Roots能直接關聯到的物件,速度快。 併發標記–進行GC Roots Tracing 的過程。 重新標記–修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發的時間短。 由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以CMS收集器的記憶體回收過程與使用者執行緒一起併發執行。 CMS的缺點: 1–》CMS收集器對CPU資源非常敏感。為了應對這種情況,虛擬機器提供了一種,“增量式併發收集器”的CMS收集器變種,所做的事情和單CPU時代搶佔式模擬多工機制的思想一樣,就是在併發標記,清理的時候讓GC執行緒、使用者執行緒交替執行,儘量減少GC執行緒的獨佔時間。但是事實證明,這個效果很一般,所以已經不提倡。 2–》CMS收集器無法處理浮動垃圾。在處理過程中,併發的使用者執行緒產生的垃圾在當次處理中無法處理,必須要在下次才能被處理。 3–》由於這是基於“標記-清除”演算法實現的收集器,所以如果沒有合理安排空間,當大物件進入java堆,由於無法安排記憶體,所以不得不提前進行一次FULL GC。

3.5.7 G1收集器 G1收集器是一款面向服務端應用的垃圾收集器。具有以下特點: 1–》並行與併發:G1能充分利用多CPU,多核環境的優勢,縮短stop the world 的停頓時間。 2–》分代收集:與其他收集器一樣,分代概念也在G1中得到保留 3–》空間整合:與CMS 的“標記-清理”不同,G1從整體看是基於“標記-整理”的,但是從區域性(兩個Region之間)看是基於複製演算法實現的。所以G1運作期間不會產生記憶體空間碎片。收集後能提供規整的可用記憶體。 4–》可預測的停頓:這是G1相對於CMS的一大優勢。G1除了追求低停頓之外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段裡,消耗在垃圾收集上的時間不超過N秒。這幾乎已經是實時JAVA(RTSJ)的垃圾收集器的特徵了。 使用G1收集器時,JAVA堆的記憶體佈局就與其他收集器有很大的區別,它將整個JAVA堆劃分為多個大小相等的獨立區域(region),雖然還保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了,他們都是一部分Region(不需要連續)的集合。 在G1虛擬機器裡,region之間的物件引用以及其他收集器中的新生代和老年代之間的物件引用,虛擬機器都是使用remembered SET 來避免全堆掃描的。G1中每個Region都有一個與之對應的remembered Set,虛擬機發現程式在對Reference型別的資料進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的物件是否處於不同的region之中,如果是,就通過CardTable吧相關引用資訊記錄到被引用物件所屬的Region的Remembered Set中。當進行記憶體回收時,在GC根節點的列舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。 如果不計算維護Remembered Set 的操作,G1收集器的運作大致可劃分為以下幾個步驟: 1-初始標記 2-併發標記 3-最終標記 4-篩選回收

3.5.8 理解GC日誌 最開始的數字:GC發生時間,從虛擬機器啟動以來經過的秒數。 GC日誌開頭的“【GC”和“【FULL GC”說明了這次垃圾收集的停頓型別。如果有FULL,就說明這次停頓是stop the world。 接下來的“DefNew”、“tenured”、“Perm”表示GC發生的區域 後面方括號裡面的“3324K->152K(3721K)”表示:“GC前該記憶體區域已使用容量->GC後該記憶體區域已使用容量(該記憶體區域總容量)”。方括號之外的“3324K->152K(11904K)”表示 GC之前JAVA堆已使用容量->GC後JAVA堆已使用容量(JAVA堆總容量) 再往後,“0.0025295 secs”表示該記憶體區域GC所佔用的時間,單位是秒。

3.6 記憶體分配與回收策略 物件的記憶體分配,往大方向講,就是在堆上分配,物件主要在新生代的Eden區。如果啟動了本地執行緒分配緩衝,將按照執行緒優先在TLAB上分配。少數情況下也可能直接分配在老年代中,其細節取決於當前使用的是哪一種垃圾收集器組合。還有虛擬機器中與記憶體相關的引數設定。

3.6.1 物件優先在Eden分配 大多數情況下,多想在新生代Eden區分配。當Eden區沒有足夠空間分配時,虛擬機器將發起一次Minor GC。 Minor GC=新生代GC指的是發生在新生代的垃圾收集動作,因為JAVA物件大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也快。 Major GC/Full GC=老年代GC。一般來說,發生一次Major GC 會伴隨著一次Minor GC。Major GC比MInor GC慢10倍以上。

3.6.2大物件直接進入老年代 所謂的大物件,就是指需要大量連續記憶體空間的物件,例如很長的字串,陣列等。虛擬機器有一個設定,讓大於這個設定值的大物件直接進入老年代。

3.6.3 長期存活的物件進入老年代 虛擬機器給每個物件定義了一個物件年齡(AGE)計數器。如果物件在Eden出生,並且經過第一次Minor GC後仍然存活,將被放置到Survivor空間,並且年齡+1。它在Survivor中每熬過一次Minor GC,年齡就增加一歲,當它的年齡大於一定值(一般預設15),就會被晉升到老年代。

3.6.4 動態物件年齡判斷 如果在Survivor中相同年齡所有物件大小大於Survivor空間的一半,年齡大於或等於該年齡的物件就直接進入老年代。

3.6.5 空間分配擔保 在發生GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間。如果這個條件成立,那麼MinorGC可以確保是安全的。如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將會嘗試一次Minor GC ,,儘管這次GC是有風險的。如果小於,或者HandlePromotionFailure設定不允許冒險,那麼改為進行一次FULL GC。 如何觸發minor GC和full GC?

Minor GC ,Full GC 觸發條件: Minor GC觸發條件:當Eden區滿時,觸發Minor GC。 Full GC觸發條件: (1)呼叫System.gc時,系統建議執行Full GC,但是不必然執行 (2)老年代空間不足 (3)方法去空間不足 (4)通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體 (5)由Eden區、From Space區向To Space區複製時,物件大小大於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小