JVM筆記(1.2)垃圾收集器和內存分配策略

垃圾收集器(GC)的做用相信你們都知道,它將咱們的不用的內存空間給回收,Java的垃圾收集器是"動態分配內存和垃圾收集"的。正由於它是動態的,因此不少人都忽略了它,但當出現一些內存泄漏、內存溢出的問題時,咱們必須掌握JVM才能去解決問題java

如今,咱們從GC設計者的角度來看它須要完成哪些工做:

  1. 哪些內存須要回收
  2. 何時回收
  3. 如何回收

對於第一個問題:

上文中,咱們說到程序計數器、虛擬機棧、本地方法棧這3個區域隨線程生,隨線程死算法

棧中的棧幀隨着方法的進入和退出而出棧、入棧,每一個棧幀分配多少內存基本在類結構肯定時就是已知的(不包括JIT的優化)數組

而Java堆和方法區只在程序運行期間纔會知道開闢的空間(),這部份內存分配和回收是動態的,因此垃圾收集器關注的這部份內存安全

對於第二個問題

當一段內存再也不使用(不處於存活狀態)時就回收,下文會談到哪些內存將再也不使用bash

對於第三個問題

這就是咱們下文要講到的各類回收機制服務器

判斷對象是否'存活'

首先,來看堆,堆中存放了幾乎全部的實例對象,在對堆進行回收內存時,要先判斷哪些對象能被回收(存活)。多線程

引用計數法

每當有一個地方引用它,計數器+1,每當一個引用失效,計數器-1;任什麼時候刻計數器爲0的對象是不能被使用的。併發

存在的問題

這種分析雖然簡單,但有一個問題,如循環引用:佈局

public class A {
    Object obj;
    
    public void testGC(){
        A a1 = new A();
        A a2 = new A();
        a1.obj = a2;
        a2.obj = a1;
        a1 = null;
        a2 = null;
        System.gc();//若是採用引用記數法則不回收
    }
}
複製代碼

可達性分析算法

因此,咱們須要一種更全面的回收機制性能

思路:

經過一系列被稱爲「GC Roots」的對象做爲起點,從這些節點開始往下搜索,搜所走過的路徑成爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連,則證實此對象是不可用的

image

可做爲GC Roots的對象:

  • 虛擬機棧中引用的對象
  • 方法去中類靜態屬性的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(native方法)引用的對象

引用

垃圾收集器判斷對象存活都和引用有關,下面來看引用有哪些類型

  • 強引用:廣泛存在,如Object obj = new Object();只要引用在,就不會回收
  • 軟引用:jdk1.2以後,SoftReference類實現軟引用。在系統發出內存溢出以前,會把這些對象二次回收,若還不夠,拋出異常。
  • 弱引用:jdk1.2以後,WeakReference來實現弱引用。只能生存到下一次垃圾收集發生以前
  • 虛引用:jdk1.2以後,PhantomReference類實現虛引用。這個對象被系統回收時收到一個通知

生存仍是死亡

一個對象要被宣告死亡,要經歷兩部:

  • 若是對象進行可達分析後沒有和GC Roots相連,那她將會被第一次標記而且進行一次篩選,若是有finalize()方法則放置在F-Queue隊列中執行finalize()方法,(不保證它有運行結果)
  • 若是是第二次被標記而且沒有引用,那就只有被回收了

一個對象的finalize()方法只會被執行一次

在Java9中,finalize()方法已被棄用,緣由以下:

  • finalize機制可能會致使性能問題,死鎖和線程掛起。
  • finalize中的錯誤可能致使內存泄漏;若是不在須要時,也沒有辦法取消垃圾回收;而且沒有指定不一樣執行finalize對象的執行順序。
  • 沒有辦法保證finlize的執行時間。

回收方法區

永久代的方法區分爲兩部分:

  • 廢棄常量(如String常量)
  • 無用的類(同時知足如下三種爲無用的類)
    • 該類全部實例都已經被回收,Java堆中不存在該類的任何實例
    • 加載該類的ClassLoader已經被回收
    • 對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問

垃圾收集算法

知道了要回收哪些東西,咱們還要知道如何回收,下面來看一下典型的垃圾回收算法

標記-清除算法

分爲標記清除兩個階段:首先標記出全部須要回收的對象,在標記完成後贊成收回被標記的對象

image

不足:
  • 效率:標記和清除兩個過程效率都不高
  • 空間:標記清除後會產生大量不連續的內存碎片(這會致使如有大對象但找不到連續內存時必須再觸發一次垃圾收集)

複製算法

他將內存分爲兩塊,每次只使用其中一塊。當一塊內存用完後,就將還存活的對象複製到另外一塊上面,再將已使用的內存一次清理掉。

image

可是,咱們通常不將它對半分,而是分爲一塊較大的Eden和兩塊較小的Survivor區域,HotSpot默認Eden:Survivor比例大小爲8:1,即Eden爲收集前的空間,一塊Survivor爲收集後的大小,只浪費了10%的空間。

注:當每次回收有大於10%的對象存活時,經過分配擔保機制讓Survivor中剩餘存不下的進入老年代

標記-整理算法

標記過程和標記-清除算法同樣,清理以前,讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存。

image

分帶收集算法

根據對象存活週期的不一樣劃分爲幾塊,通常爲新生代和老年代

  • 新生代中,有大量對象死去,用複製算法
  • 老年代中,存活率高,必須使用標記-清理或者標記-整理算法來回收

HotSpot算法實現

以上爲理論的垃圾收集算法,實際如HotSpot虛擬機會對算法有嚴格的考量。。。

枚舉根節點

時間消耗:
  • 查找GC Roots節點
  • GC停頓,整個分析期間整個執行系統就像被凍結在某個時間點上,由於查找時不能出現分析時對象過程稿還在不停變化的狀況

OopMap:虛擬機用它來得知哪些地方存放着對象引用,在類加載完成後,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定位置記錄棧和寄存器中哪些位置是引用

安全點

程序在安全點才能暫停執行GC,因此安全點通常選定爲「是否具備讓程序長時間執行的特徵」(如方法調用,循環跳轉,異常跳轉),前文「特定位置」就被稱爲安全點。

如何讓全部線程都跑到最近安全點上停下來:

  • 搶先式中斷:把全部線程中斷,若是有中斷線程不在安全點上,恢復線程,讓它跑回安全點上(幾乎沒有了)
  • 主動式中斷:設置一個標誌,讓各個線程去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起,輪詢標誌的地方和安全點重合。

安全區域(Safe Region)

安全點保證了程序執行時在不太長的時間內就會遇到可進入的GC的安全點,例如線程處於SLeep或Blocked狀態,這時線程沒法響應JVM中斷請求,這種狀況,就須要安全區域來解決。

安全區域是指在一段代碼中,引用關係不會發生變化。

當線程執行到了安全區域中的代碼,標識本身進入了Safe Region,擋在這段時間裏JVM要發起GC時,就不用管標識本身爲Safe Region狀態的線程了。在線程要離開Safe Region時,去檢查是否完成根節點枚舉,若是完成,線程繼續執行;不然它就必須等待直到收到能夠安全離開Safe Region的信號爲止。

垃圾收集器

能夠理解爲收集算法是內存回收的方法論,垃圾收集器就是內存回收的具體實現

image

Serial收集器

特色:

  • 單線程收集器:在他進行垃圾收集時,必須暫停其餘工做線程,直到它收集結束

image

ParNew收集器

特色:

  • Serial收集器的多線程版本,除了Serial收集器外,只有它能和CMS收集器合做
    image

Parallel Scavenge收集器

特色:

  • Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器
  • 該收集器的目標是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即 吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
  • 自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它一樣是一個單線程收集器,使用標記整理算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。

若是在Server模式下,主要兩大用途:

  • (1)在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用
  • (2)做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用

image

Parallel Old收集器

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。這個收集器在1.6中才開始提供。

image

CMS收集器

這類應用尤爲重視服務器的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就很是符合這類應用的需求

CMS收集器是基於「標記-清除」算法實現的。它的運做過程相對前面幾種收集器來講更復雜一些,整個過程分爲4個步驟:

  • 初始標記:標記一下GC Roots能直接關聯的對象,速度快
  • 併發標記:GC Roots Tracing的過程
  • 從新標記:修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,通常比初始標記階段稍長,但比並發標記時間短
  • 併發清除:清除

其中,初始標記、從新標記這兩個步驟仍然須要「Stop The World」.

CMS收集器主要優勢:

  • 併發收集
  • 低停頓。

CMS三個明顯的缺點:

  • CMS收集器對CPU資源很是敏感。CPU個數少於4個時,CMS對於用戶程序的影響就可能變得很大,爲了應付這種狀況,虛擬機提供了一種稱爲「增量式併發收集器」的CMS收集器變種。所作的事情和單CPU年代PC機操做系統使用搶佔式來模擬多任務機制的思想
  • CMS收集器沒法處理浮動垃圾,可能出現「Concurrent Mode Failure」失敗而致使另外一次Full GC的產生。"浮動垃圾"是指CMS併發清理時用戶線程還在運行,伴隨程序運行有新垃圾出現,這一部分垃圾在標記以後出現,因此本次沒法清理。在JDK1.5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活,這是一個偏保守的設置,若是在應用中藍年代增加不是太快,能夠適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提升觸發百分比,以便下降內存回收次數從而獲取更好的性能,在JDK1.6中,CMS收集器的啓動閥值已經提高至92%。
  • CMS是基於「標記-清除」算法實現的收集器,收集結束時會有大量空間碎片產生。空間碎片過多,可能會出現老年代還有很大空間剩餘,可是沒法找到足夠大的連續空間來分配當前對象,不得不提早出發FullGC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認就是開啓的),用於在CMS收集器頂不住要進行FullGC時開啓內存碎片合併整理過程,內存整理的過程是沒法併發的,空間碎片問題沒有了,但停頓時間變長了。虛擬機設計者還提供了另一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,標識每次進入Full GC時都進行碎片整理)

G1收集器

G1收集器的優點:
  • 並行與併發 (停頓時間少)
  • 分代收集 (採用不一樣的收集方式處理不一樣年代的堆)
  • 空間整理 (標記整理算法,複製算法)
  • 可預測的停頓 (讓使用者能控制一次收集的時間長度👏)
G1採用的堆佈局:

使用G1收集器時,Java堆的內存佈局是整個規劃爲多個大小相等的獨立區域(稱爲Region,大小爲2的冪次方,如1M,2M,4M),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region的集合。

可以預測停頓的時間的緣由:

G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在真個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲取的空間大小以及回收所須要的時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的又來)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內能夠獲取儘可能可能高的收集效率

G1 內存「化整爲零」的思路:

問:若一個對象在Region中,但他若是有其餘Region中、甚至整個堆任意對象有引用關係,作可達性斷定對象存活時,要掃描整個對空間嗎?

答:

  1. 虛擬機經過Remembered Set避免全堆掃描,每一個Region都有與之對應的Remembered Set。
  2. 當程序對引用類型進行寫操做時,會產生一個Write Barrier暫停中斷寫操做,檢查引用的對象是否處於Region之中。是,就經過CardTable將引用信息記錄到所屬Region的Remember Set中。
  3. 回收時,Remembered Set就能夠保證不用進行全堆掃描了。
若是不計算維護Remembered Set的操做,G1收集器的運做大體可劃分爲一下步驟:
  • 初始標記:標記GC Roots能直接關聯的對象,修改Next Top at Mark Start的值,讓下一階段程序運行時,在正確的Region中建立對象,停頓線程,耗時短。
  • 併發標記:從GC Root對堆對象進行可達性分析,找存活對象,可與用戶線程併發執行,耗時長。
  • 最終標記:修正併發標記因用戶程序繼續運做致使標記變更的部分,JVM將這段變化記錄在Remembered Set Logs中,最終標記階段將Remembered Set Logs數據合併到Remembered Set中。
  • 篩選回收:對各個Region的回收價值和成本進行排序,根據用戶指望GC停頓時間制定回收計劃。
G1的回收模式

G1中提供了三種模式垃圾回收模式,young gc、mixed gc 和 full gc,在不一樣的條件下被觸發。

  • Young GC:通常對象(除了巨型對象)都是在eden region中分配內存,當全部eden region被耗盡沒法申請內存時,就會觸發一次young gc,這種觸發機制和以前的young gc差很少,執行完一次young gc,活躍對象會被拷貝到survivor region或者晉升到old region中,空閒的region會被放入空閒列表中,等待下次被使用。
  • 回收整個young region,還會回收一部分的old region
  • 老年代被填滿,就會觸發一次full gc(儘可能避免)

image

內存分配與回收策略

自動內存管理最終能夠歸結爲自動化地解決了兩個問題:

  • 給對象分配內存
  • 回收分配給對象的內存

簡單來講,對象內存分配主要是在堆中分配。可是分配的規則並非固定的,取決於使用的收集器組合以及JVM內存相關參數的設定

對象優先在Eden分配

大多數狀況下,對象在Eden區分配內存

Minor GC和Full GC的區別:

  • 新生代GC(Minor GC):指發生在新生代的垃圾回收動做,頻繁,回收速度也快
  • 老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次Minor GC(並不是絕對,在Parallel Scavenege收集器的收集策略裏就有進行Major GC的策略過程選擇),它的速度通常比Minor GC慢十倍。

大對象直接進入老年代

大對象是指,須要連續內存空間的Java對象,例如很長的字符串或數組

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

虛擬機給每一個對象定義了一個對象年齡(Age)計數器,若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,會被移動到Survivor空間中,而且對象年齡爲1.每在Survivor區中渡過一次Minor GC,年齡增長1,當它的年齡增長到必定程度(默認15),就被晉升到老年代。

動態對象年齡斷定

若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代

空間分配擔保

僞代碼解釋:

//準備Minor GC:
    if (老年代中最大連續可用空間>新生代全部對象總空間){
        //開始Minor GC
    } else {
        if (容許擔保失敗){
            if (老年代最大連續可用空間>歷次晉升老年代對象平均大小){
                //開始Minor GC
            } else {
                //開始Full GC
            }
        } else {
            //開始Full GC
        }
    }
複製代碼

通常來講,新生代只使用一個survivor空間來進行輪換時的備份,因此當出現極端狀況(即新生代空間在一次minor GC後所有存活)時survivor空間有可能爆滿,因此此時須要老年代進行分配擔保,即survivor區沒法容納的對象都進入老年代。

在JDK 6 Updale 24 以後,Handle PromotionFailure 不會再影響到虛擬機的空間分配擔保策略,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。

總結

最後,內存回收和垃圾收集器不少時候都是影響系統性能,併發能力的緣由,虛擬機也提供了多種收集器和大量的調節參數,由於不少時候咱們要選擇本身的業務來設置相應的收集方式

相關文章
相關標籤/搜索