Java 8 記憶體管理原理解析及記憶體故障排查實踐

vivo互联网技术發表於2024-03-21

作者:vivo 網際網路伺服器團隊- Zeng Zhibin

介紹Java8虛擬機器的記憶體區域劃分、記憶體垃圾回收工作原理解析、虛擬機器記憶體分配配置,介紹各垃圾收集器優缺點及場景應用、實踐記憶體故障場景排查診斷,方便讀者面臨記憶體故障時有一個明確的思路和方向。

一、背景

Java是一種流行的程式語言,可以在不同的作業系統上執行。它具有跨平臺、物件導向、自動記憶體管理等特點,Java程式在執行時需要使用記憶體來儲存資料和程式狀態。

Java的自動記憶體管理機制是由 JVM 中的垃圾收集器來實現的,垃圾收集器會定期掃描堆記憶體中的物件,檢測並清除不再使用的物件,以釋放記憶體資源。

Java的自動記憶體管理機制帶來了許多好處,首先,它可以避免程式設計師手動管理記憶體時的錯誤,例如記憶體洩漏和懸空指標等問題。其次,它可以提高程式的執行效率,因為程式設計師不需要頻繁地手動分配和釋放記憶體,而是可以將更多時間和精力專注於程式的業務邏輯,最後,它可以提高程式的可靠性和穩定性,因為垃圾收集器可以自動檢測和清除不再使用的記憶體資源,避免記憶體溢位等問題。

瞭解和掌握垃圾收集器原理可以幫助提高程式的效能、穩定性和可維護性。

名詞解釋:

響應速度:響應速度指程式或系統對一個請求的響應有多迅速。比如,使用者查詢資料響應時間,對響應速度要求很高的系統,較大的停頓時間是不可接受的。

吞吐量:吞吐量關注在一個特定時間段內應用系統的最大工作量,例如每小時批處理系統能完成的任務數量,在吞吐量方面最佳化的系統,較長的GC停頓時間也是可以接受的,因為高吞吐量應用更關心的是如何儘可能快地完成整個任務,不考慮快速響應使用者請求。

GC導致的應用暫停時間影響系統響應速度,GC處理執行緒的CPU使用率影響系統吞吐量。

二、Java 8 的記憶體管理

2.1 JVM(Java虛擬機器)記憶體劃分

Java執行時資料區域劃分,Java虛擬機器在執行Java程式時,將其所管理的記憶體劃分為不同的資料區域,每個區域都有特定的用途和建立銷燬的時間。其中,有些區域在虛擬機器程序啟動時就存在,而有些區域則是隨著使用者執行緒的啟動和結束而建立和銷燬。這些資料區域包括程式計數器、虛擬機器棧、本地方法棧、堆、方法區等,每個區域都有其自身的特點和作用。瞭解這些資料區域的使用方式和特點,可以更好地理解Java虛擬機器的記憶體管理機制和執行原理。

JVM的記憶體區域劃分可分為:1.堆記憶體空間、2.Java虛擬機器棧區域、3.程式計數器、4.本地方法棧、5.元空間區域、6.直接記憶體。

圖片

圖片

  • 堆記憶體空間:JVM中佔用記憶體空間最大的是堆,平常物件的建立大部分都是在堆上分配記憶體的,是垃圾回收的主要目標和方向。

  • 本地方法棧區域:Native Mehod Stack與Java虛擬機器棧的作用非常相似,區別是Java虛擬機器棧為虛擬機器執行Java方法或者為位元組碼而服務,本地方法棧是為了Java 虛擬機器棧得到Native方法。

  • Java虛擬機器棧區域:負責Java的解釋過程、程式的執行過程、入棧和出棧,它是與執行緒相關的,當啟動一個新的執行緒時,Java程式就會分配一個Java 虛擬機器棧提供執行;Java 虛擬機器棧從方法入棧到具體位元組碼執行是一個雙層棧結構,可以棧裡包含棧。

  • 程式計數器:記錄執行緒執行位置,執行緒私有,因為作業系統不停的排程,無法獲取到執行緒被排程之前的位置,程式計數器提供了這樣一個執行緒執行位置。

  • 元空間區域:在原來的老的Java 7之前劃分中,永久代用來存放類的後設資料資訊、靜態變數以及常量池等。在現在Java8後類的元資訊儲存在元空間中,靜態變數和常量池等併入堆中,相當於原來的永久代中的資料,被元空間和堆記憶體給瓜分了。

  • 直接記憶體:使用了Java 的直接記憶體的API的記憶體,例如緩衝ByteBuffer,可以控制虛擬機器引數調整大小,而本地記憶體是使用了native函式操作的記憶體,是不受JVM管理控制。

堆記憶體空間

JVM回收的主要目標是堆記憶體,物件主要的建立分配記憶體在堆上進行,堆可以想象成一個物件池子,物件不停建立放入池子中,而JVM垃圾回收是不停的回收池子中一些被標記為可回收物件的物件,啟動回收執行緒進行打掃戰場,當回收物件的速度趕不上程式的建立時,池子就會立馬滿,當滿了之後從而發生溢位,就是常見的OOM。

GC的速度和堆的記憶體中存活物件的數量有關,與堆記憶體所有的物件無關,GC的速度和堆記憶體的大小無關,如一個4GB大小的堆記憶體和一個16GB的堆記憶體,只要2個堆記憶體存活物件都是一樣多的時候,GC速度都是基本差不多。每次垃圾回收也不是必須要把垃圾清理乾淨,重要的是保證不把正在使用的物件給標記清除掉。

2.2 堆記憶體管理

JVM中佔用記憶體空間最大的是堆記憶體,平常物件的建立大部分都是在堆上分配記憶體的,是Java垃圾回收的主要目標和方向、是 Java記憶體管理機制的核心組成部分,它可以自動管理 Java程式的記憶體分配和釋放,Java垃圾收集器可以自動檢測和回收不再使用的記憶體,以便重新分配給其他需要記憶體的程式。這種自動記憶體管理的機制可以提高程式的執行效率和可靠性,防止因記憶體洩漏等問題導致程式崩潰或效能下降,Java 垃圾收集器使用了不同的垃圾回收演算法和垃圾收集器實現,以適應不同的應用場景和需求。Java垃圾收集器的效能特徵和最佳化技術也是 Java程式設計師需要了解和掌握的重要知識。

因此,瞭解 Java垃圾回收的背景、原理和實踐經驗對於編寫高效、可靠的 Java程式非常重要。

2.2.1 物件如何被判斷為可回收

JVM怎麼判斷堆記憶體裡面的物件是否可回收的,就是當一個物件沒有任何引用指向它了,它就是可回收物件,判斷的方式有兩種演算法,一個是引用計數法,一個是可達性分析法。

可回收物件:

圖片

(1)引用計數法

給物件中新增一個引用計數器,每當有一個地方引用它時,這個計數器值加一,當引用失效斷開時,計數器值就減一,在任何時刻時計數器為0的時候,代表這個物件是可以被回收的,沒有任何引用使用它了。

圖片

引用計數法是有缺點,當物件直接互相依賴引用時,這些物件的計數器都不能為0,都不能被回收。

(2)可達性分析法

它使用tracing(鏈路追蹤)方式尋找存活物件的方法,透過一些列稱為“GC Roots”的物件作為初始點,從這些初始點開始向下查詢,直到向下查詢沒有任何鏈路時,代表這個物件可以被回收,這種演算法是目前Java唯一且預設使用來判定可回收的演算法。

圖片

2.2.2 GC Roots的概念和物件型別

  1. Java 虛擬機器棧中引用的物件,例如各個執行緒被呼叫的方法棧用到的引數、區域性變數或者臨時變數等。

  2. 方法區的靜態類屬性引用物件或者說Java類中的引用型別的靜態變數。

  3. 方法區中的常量引用或者執行時常量池中的引用型別變數。

  4. JVM內部的記憶體資料結構的一些引用、同步的監控物件(被修飾同步鎖)。

  5. JNI中的引用物件。

當然,被GC Roots追溯到的物件不是一定不會被垃圾回收,具體需要看情況,Java 物件與物件引用存在四種引用級別:分別是強引用、軟引用、弱引用、虛引用,預設的物件關係是強引用,只有在和GCRoots沒有關係時才會被回收;軟引用用於維護一些可有可無的物件,當記憶體足夠時不會被回收;弱引用只要發生了垃圾回收就會被清理;虛引用人如其名形同虛設,任何物件都與它無關。

2.2.3 垃圾物件回收演算法

當JVM定位到了那些物件可回收時,這個時候是透過三個演算法標記清除,分別是標記清除演算法、複製演算法、標記壓縮演算法。

(1)標記清除演算法

首先標記出所有需要回 收的物件,在標記完成後,統一回收掉所有被標記的物件,但是該演算法缺點是執行效率低,當大量物件時需要大量標記和清理動作,而且容易產生記憶體碎片化,當需要一塊連續記憶體時,會因為碎片化無法分配。

圖片

(2)標記壓縮演算法

標記壓縮演算法跟清除演算法很像,只不過它對記憶體進行了整理, 讓存活物件都向記憶體空間的一端移動,然後將邊界的其它物件全部清理,這樣能達到記憶體碎片化問題,不過它比清除演算法多了移步動作。

圖片

(3)複製演算法

為了解決標記-清除演算法面對大量可回收物件時執行效率低的問題,將存活物件複製到一塊空置的空間裡,然後將原來的區域全部清理,缺點是需要額外空間存放存活物件。

圖片

2.2.4 分代垃圾回收模型概念和原理

堆記憶體分代模型圖

圖片

當JVM進行GC(垃圾回收)時,JVM會發起“Stop the world”,所有的業務執行緒都進行停止,進入SafePoint狀態,JVM回收垃圾執行緒開始進行標記和追溯,如何解決這種停止和如何減少STW的時間呢?

目前主流垃圾收集器採用分代垃圾回收方式,大部分物件的宣告週期都比較短,只有少部分的物件才存活的比較長,分代垃圾回收會在邏輯上把堆記憶體空間分為兩部分,一部分為年輕代,一部分為老年代。

(1)年輕代空間

年輕代主要是存放新生成的物件,一般佔用堆空間的三分之一空間,因為會頻繁建立物件,所以年輕代GC頻率是最高的。

分為Eden空間、Survivor1(from)區、Survivor2(to)區,S1和S2總要有一塊空間是空的,為了方便年輕代存活物件來回存放,晉升存活物件年齡。

三個區的預設比例是8:1:1,可以透過配置引數調整比例。

年輕代回收發起Minor GC(YongGC),當Eden記憶體區域被佔滿之後就發起GC,短暫的STW,基於垃圾收集器。

(2)老年代空間

是堆記憶體中最大的空間, ,裡面的物件都是比較穩定或者老頑固,GC頻率不會頻繁執行。

圖片

老年代物件:

  1. 正常提升:由年輕代存活物件年齡到達閾值時,這個物件則會被移動到老年代中。

  2. 分配擔保:如果年輕代中的空間不足時,此時有新的物件需要分配物件空間,需要依賴其它記憶體進行分配擔保,老年代擔保直接建立。

  3. 大物件:當建立需要大量連續記憶體空間的物件時,如長字串或者陣列等,大小超過了閾值時,直接在老年代分配。

  4. 動態年齡物件:有的垃圾收集器不需要到達指定年齡大小直接晉升老年代,比如相同年齡的物件的大小總和 > Survivor空間的50%, 年齡大於等於該年齡物件直接移動老年代,無需等待正常提升。

老年代回收發起Major GC / FULL GC,當老年代滿時會觸發MajorGC,通常至少經歷過一次Minor GC,再緊接著進行Major GC, Major GC清理Tenured區,用於回收老年代(CMS才能單獨清理)。

FUll GC:清除整個堆空間,一般來說是針對整個新生代、老生代、元空間的全域性範圍的清理。

不管是Major GC還是 Full GC, STW的耗時都是Ygc的十倍以上,所以說物件能在年輕代被回收是最優的。

Full GC觸發條件:

  • 老年代空間不足。

  • 元空間不足擴容導致。

  • 程式程式碼執行System.gc時可能會執行。

  • 當程式建立一個大物件時,Eden區域放不下大物件,老年代記憶體擔保分配,老年代也不足空間時。

  • 年輕代存留物件晉升老年代時,老年代空間不足時。

2.2.5 Java物件記憶體分配過程

圖片

物件的分配過程

  1. 編譯器透過逃逸分析最佳化手段,確定物件是否在棧上分配還是堆上分配。

  2. 如果在堆上分配,則確定是否大物件,如果是則直接進入老年代空間分配, 不然則走3。

  3. 對比tlab, 如果tlab_top + size <= tlab_end, 則在tlab上直接分配,並且增加tlab_top值,如果tlab不足以空間放當前物件,則重新申請一個tlab嘗試放入當前物件,如果還是不行則往下走4。

  4. 分配在Eden空間,當eden空間不足時發生YGC, 倖存者區是否年齡晉升、動態年齡、老年代剩餘空間不足發生Full GC 。

  5. 當YGC之後仍然不足當前物件放入,則直接分配老年代。

TLAB作用原理:Java在記憶體新生代Eden區域開闢了一小塊執行緒私有區域,這塊區域為TLAB,預設佔Eden區域大小的1%, 作用於小物件,因為小物件用完即丟,不存線上程共享,快速消亡GC,JVM優先將小物件分配在TLAB是執行緒私有的,所以沒有鎖的開銷,效率高,每次只需要執行緒在自己的緩衝區分配即可,不需要進行鎖同步堆 。

物件除了基本型別的不一定是在堆記憶體分配,在JVM擁有逃逸分析,能夠分析出一個新的物件所擁有的範圍,從而決定是否要將這個物件分配到堆上,是JVM的預設行為;Java 逃逸分析是一種最佳化技術,可以透過分析 Java 物件的作用域和生命週期,確定物件的記憶體分配位置和生命週期,從而減少不必要的記憶體分配和垃圾回收。可以在棧上分配,可以在棧幀上建立和銷燬,分離物件或標量替換,同步消除。

public class TaoYiFenxi {
 
    Object obj;
 
    public void setObj() {
        obj = new Object();
    }
 
    public Object getObject() {
        Object obj1 = new Object();
        return obj1;
    }
 
 
    public void test1() {
        synchronized (new Object()) {
 
        }
    }
 
}

2.2.6 JVM垃圾收集器特點與原理

(1)Serial垃圾收集器、Serial Old垃圾收集器

圖片

Serial收集器採用複製演算法, 作用在年輕代的一款垃圾收集器,序列執行,執行過程中會STW,是使用單個執行緒進行垃圾回收,響應速度優先。

Serial Old 收集器採用標記整理演算法,作用在老年代的一款收集器,序列執行,執行過程中會暫停所有使用者執行緒,會STW,使用單個執行緒進行垃圾回收,響應速度優先。

  • 使用場景:適合記憶體小几十兆以內,比較適合簡單的服務或者單CPU服務,避免了執行緒互動的開銷。

  • 優點:小堆記憶體且單核CPU執行效率高。

  • 缺點:堆記憶體大,多核CPU不適合,回收時長非常長。

(2)Parallel Scavenge垃圾收集器、Parallel Old垃圾收集器

圖片

Parallel Scavenge垃圾收集器採用了複製演算法,作用在年輕代的一款垃圾收集器,是並行的多執行緒執行,執行過程中會發生STW,關注與程式吞吐量。

Parallel Old垃圾收集器採用標記整理演算法,作用,作用在老年代的一款垃圾收集器, 是並行的多執行緒執行,執行過程中會發生STW,關注與程式吞吐量。

Parallel Scavenge + Parallel Old組合是Java8當中預設使用的一個組合垃圾回收。

所謂的吞吐量是CPU用於執行使用者程式碼時間與CPU總消耗時間的比值,也就是說吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集器時間), 錄入程式執行了100分鐘,垃圾收集器花費時間1分鐘,則吞吐量達到了99%。

  • 使用場景:適用於記憶體在幾個G之間,適用於後臺計算服務或者不需要太多互動的服務,保證吞吐量的服務。

  • 優點:可控吞吐量、保證吞吐量,並行收集。

  • 缺點:回收期間STW,隨著堆記憶體增大,回收暫停時間增大。

(3)Par New垃圾收集器

Par New垃圾收集器採用了複製演算法,作用在年輕代的一款垃圾收集器, 也是並行多執行緒執行,跟Parallel非常相似,是它的增強版本,或者說是Serial收集器的多執行緒版本,是搭配CMS垃圾收集器特製的一個收集器。

  • 使用場景:搭配CMS使用

(4)CMS垃圾收集器

CMS是一款多執行緒+分段操作的一款垃圾收集器。其最大的優點就是將一次完整的回收過程拆分成多個步驟,並且在執行的某些過程中可以使使用者執行緒可以繼續執行,分別有初始標記,併發標記,重新標記,併發清理和併發重置。

圖片

CMS是一款多執行緒+分段操作的一款垃圾收集器。其最大的優點就是將一次完整的回收過程拆分成多個步驟,並且在執行的某些過程中可以使使用者執行緒可以繼續執行,分別有初始標記,併發標記,重新標記,併發清理和併發重置。

CMS分段

  • 初始標記階段, 這個階段會暫停使用者執行緒, 掃描所有的根物件,因為根物件比較少,所以一般stw時間都非常短。

  • 併發標記階段,這個階段與使用者執行緒一起執行,會一直沿著根往下掃描,不停的識別物件是否為垃圾,標記,採用了三色演算法, 在物件頭(Mark World)標識了一個顏色屬性,不同的顏色代表不同階段,掃描過程中給與物件一個顏色,記錄掃描位置,防止cpu時間片切換不需要重新掃描。

  • 重新標記階段, 這個階段暫停使用者執行緒, 修正一些漏標物件,回掃發生引用變化的物件。

  • 併發清理階段, 這個階段與使用者執行緒一起執行,標記清除已經成為垃圾的物件。

三色標記

  • 黑色:代表了自己已經被掃描完畢,並且自己的引用物件也已經確定完畢。

  • 灰色:代表自己已經被掃描完畢了, 但是自己的引用還沒標記完。

  • 白色:則代表還沒有被掃描過。

標記過程結束後,所有未被標記的物件都是不可達的,可以被回收。

圖片

三色標記演算法的問題場景:當業務執行緒做了物件引用變更,會發生B物件不會被掃描,當成垃圾回收。

public class Demo3 {
 
    public static void main(String[] args) {
        R r = new R();
        r.a = new A();
        B b = new B();
        // GCroot遍歷R, R為黑色, R下面的a引用鏈還未掃完置灰灰色,R.b無引用, 切換時間分片
        r.a.b = b;
        // 業務執行緒發生了引用改變, 原本r.a.b的引用置為null
        r.a.b = null;
        // GC執行緒回來繼續上次掃描,發現r.a.b無引用,則認為b物件無任何引用清除
        r.b = b;
        // GC 回收了b, 業務執行緒無法使用b
    }
}
 
class R {
    A a;
    B b;
}
 
class A {
    B b;
}
 
class B {
}

圖片

當GC執行緒標記A時,CPU時間片切換,業務執行緒進行了物件引用改變,這時候時間片回到了GC執行緒,繼續掃描物件A, 發現A沒有任何引用,則會將A賦值黑色掃描完畢,這樣B則不會被掃描,會標記B是垃圾, 在清理階段將B回收掉,錯誤的回收正常的物件,發生業務異常。

CMS基於這種錯誤標記的解決方案是採取寫屏障 + 增量更新Incremental Update , 在業務執行緒發生物件變化時,重新將R標識為灰色,重新掃描一遍,Incremental Update 在特殊場景下還是會產生漏標。

圖片

public class Demo3 {
 
    public static void main(String[] args) {
        // Incremental Update還會產生的問題
        R r = new R();
        A a = new A();
        A b = new A();
        r.a1 = a;
        // GC執行緒切換, r掃完a1, 但是沒有掃完a2, 還是灰色
        r.a2 = b;
        // 業務執行緒發生引用切換, r置灰灰色(本身灰色)
        r.a1 = b;
        // GC執行緒繼續掃完a2, R為黑色, b物件又漏了~
    }
}
 
class R {
    A a1;
    A a2;
}
 
class A {
}

當GC 1執行緒正在標記O, 已經標記完O的屬性 O.1, 準備標記O.2時,業務執行緒把屬性O,1 = B,這時候將O物件再次標記成灰色, GC 1執行緒切回,將O.2執行緒標記完成,這時候認為O已經全部標記完成,O標記為黑色, B物件產生了漏標, CMS針對Incremental Update產生的問題,只能在remark階段,暫停所有執行緒,將這些發生過引用改變過的,重新掃描一遍。

  • 使用場景:適用於網際網路或者 B/S服務, 響應速度優先,適合6G左右。

  • 優點:併發收集, 低停頓,回收過程中最耗時的是併發標記和併發清除,它都能與使用者執行緒保持一起工作。

  • 缺點:

收集器對CPU的資源非常敏感,會佔用使用者執行緒部分使用,導致程式會變得緩慢,吞吐量下降。

無法處理浮動垃圾,在併發清理階段使用者執行緒還是在執行,這時候產生的新垃圾無法在這次當中處理,只有等待下次才會清理。

因為CMS使用了Incremental Update,remark階段還是會所有暫停,重新掃描發生引用改變的GC root,效率慢耗時高。

因為收集器是基於標記清除演算法實現的,所以在收集器回收結束後,記憶體會產生碎片化,當碎片化非常嚴重的時候,這時候有大物件進入無法分配記憶體時會觸發FullGC,特殊場景下會使用Serial收集器,導致停頓不可控。

(5)G1垃圾收集器

G1也是採用三色標記分段式進行回收的演算法, 不過它是寫屏障 + STAB快照實現,G1設定的目標是在延遲可控(低暫停)的情況下獲得儘可能高的吞吐量,仍然可以透過併發的方式讓Java 程式繼續執行,G1垃圾收集器在很多方面彌補了CMS的不足,比如CMS使用的是mark-sweep標記清除演算法,自然會產生記憶體碎片(CMS只能在Full GC時,STW 整理記憶體碎片),然而G1整體來看是基於標記整理演算法實現的收集器,但是從區域性來看也是基於複製演算法實現的,高效的整理剩餘記憶體,而不需要管理記憶體碎片它。

G1同樣有年輕代和老年代的概念,只不過物理空間劃分已經不存在,邏輯分割槽還存在,G1會把堆切成若干份,每一份當作一個目標,在部分上目標很容易達成,G1在進行垃圾回收的時候,將會根據最大停頓時間設定值動態選取部分小堆區垃圾回收。

圖片

G1的特點是儘量追求吞吐量,追求響應時間,併發收集,壓縮空閒空間不會延長GC暫停時間,更容易預測GC暫停時間,能充分利用CPU、多核環境下的硬體優勢,使用多個CPU對STW進行控制(200ms以內)靈活的分割槽回收,優先回收花費時間少的或者垃圾比例高的region新老比例也是動態調整,不需要配置;年齡晉升也是15,但是可以動態年齡,當倖存者region超過了50時,會把年齡最大的放入老年代。

G1動態Y區域設定,G1每個分割槽都可能是年輕代或者老年代,但是同一時刻只屬於一個代,分代概念還存在,邏輯上分代方便複用以前分代邏輯,在物理上不需要連續,這樣能帶來額外好處,有的分割槽內垃圾比較多,有的分割槽比較少,G1會優先回收垃圾比較多的分割槽,這樣可以花費少量的時間來回收這些分割槽垃圾,即收集最多垃圾分割槽;但是新生代回收不適合這種,新生代達到閾值時發生YGC,對整個新生代進行回收或者晉升倖存,新生代也分割槽是方便動態調整分割槽大小,在進行垃圾回收時,會將存活物件複製到另一個可用分割槽上,這樣也能避免一定程度的記憶體碎片化過程,每個分割槽的大小都是在1M- 32M之間,取決2的冪次方。

Humingous:如果一個物件佔用的空間超過了分割槽容量50%以上,G1收集器就認為這是一個巨型物件。這些巨型物件,預設直接會被分配在年老代,但是如果它是一個短期存在的巨型物件,就會對垃圾收集器造成負面影響;為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型物件。如果一個H區裝不下一個巨型物件,那麼G1會尋找連續的H分割槽來儲存。為了能找到連續的H區,有時候不得不啟動Full GC。

CardTable:記錄每一塊card記憶體區域是否dirty,如果在發生YGC時,怎麼知道那些是存活物件,並且其它代區域有沒有引用這部分物件,於是把記憶體劃分了很多card區域, 每個區域大小不超過512b,當該card區域裡的物件有引用關係,將當前card置為“dirty”, 並且使用卡表(CardTable)來記錄每一塊card是否dirty,在進行GC時,不用遍歷所有的空間, 只需要遍歷卡表中為"dirty"或者說布林符合條件的card區域進行回掃。

圖片

CSet:Collection SET用於記錄可被回收分割槽的集合組, G1使用不同演算法,動態的計算出那些分割槽是需要被回收的,將其放到CSet中,在CSet當中存活的資料都會在GC過程中複製到另一個可用分割槽,CSet可以是所有型別分割槽,它需要額外佔用記憶體,堆空間的1%。

RSet:RememberedSet 每個Region都有一個Rset,是一個記錄了其他Region中的物件到本身Region的引用,它可以使得垃圾收集器不需要掃描整個堆去找到誰的引用了當前分割槽物件,是G1高效回收的關鍵點,也是三色演算法的一個以來點。

圖片

RSet和卡表的區別是什麼?

卡表記錄的是堆記憶體中card有沒有變成"dirty", 但是它本身不知道dirty裡面哪些是引用了的物件,它是一個大維度的一個記錄,RSet是記錄自身Region中物件引用了其它Region中的那些物件,詳細的記錄對方引用物件資訊,G1使用了兩者的結合,實現了增量式的垃圾回收,並最佳化跨區引用的最終處理。

SATB演算法:是一種基於快照的演算法,它可以避免在垃圾回收時出現物件漏標或者重複標記的問題,從而提高垃圾回收的準確性和效率,在垃圾回收開始時,對堆中的物件引用進行快照,然後在併發標記階段中記錄下所有被修改過物件引用,儲存到satb_mark_queue中,最後在重新標記階段重新掃描這些物件,標記所有被修改的物件,保證了準確性和效率。

SATB演算法在remark階段不需要暫停遍歷整個堆物件,只需要掃描“satb_mark_queue”佇列中的記錄,避免了這個階段長耗時,而cms的增量演算法在這個階段是需要重新掃描GC Roots標記整個堆物件,導致了不可控時間暫停,總的來說G1是透過回收領域應用並行化策略,將原來的幾塊大記憶體塊回收問題,演變成了N個小記憶體塊回收,使得回收效率可以高度並行化,停頓時間可控,可以與使用者執行緒併發執行,將一塊記憶體分而治之。

圖片

G1預設當分割槽記憶體佔用閾值達到總記憶體的45%,會發生Mixed gc(混和GC),YoungGC + 併發回收Mixed GC過程:初始標記(stw)、併發標記、最終標記(重新標記stw)、篩選回收(stw並行)。

  • 使用場景:響應速度優先,較高的吞吐量,面向服務端,使用記憶體6G以上。

  • 優點:並行與併發收集,分代分割槽收集,優先垃圾收集,空間整合,可控或者可預測停頓時間。

  • 缺點:

收集中產生記憶體,G1的每個region都需要有一份記憶集和卡表記錄跨代指標,這導致記憶集可能佔用堆空間10-20%甚至更多空間。

執行過程中額外負載開銷加大,寫屏障進行維護卡表操作外,還需要原始快照能夠減少併發標記和重新標記階段的消耗,避免最終標記階段停頓過長,執行過程中會產生由跟蹤引用變化帶來的額外開銷負擔,比CMS增量演算法消耗更多,CMS的寫屏障實現直接是同步操作, 而G1是把寫屏障和寫後屏障中要做的事情放到佇列裡非同步處理。

G1對於Full GC是沒有處理流程, 一旦發生Full GC G1的回收執行的是單執行緒的Serial回收器進行回收。

2.2.7 垃圾收集器配置使用

機器配置:64位 4C8G

Java 程式使用CMS收集器進行記憶體垃圾回收初始記憶體劃分情況:

-Xms4096M -Xmx4096M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/{runuser}/logs/other -XX:+UseConcMarkSweepGC

圖片

CMS 跟 parNew佔比情況, 預設下 ParNew佔用整個堆的空間為:機器位數 * CPU核數 * 13 /10 , 當前機器配置計算得出 64 * 4 * 13 / 10 = 332M , 與圖上數值差別不大。

Java程式使用G1收集器進行記憶體垃圾回收初始記憶體劃分情況:

-Xms4096M -Xmx4096M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/{runuser}/logs/other -XX:+UseG1GC

圖片

G1 新老年代的佔比是動態調整, 隨著執行時根據實際情況劃分空間。

Java8預設ParallerGC收集器初始記憶體劃分情況:

圖片

parallel GC回收器預設堆old區與young區記憶體大小比例 2:1, 圖上數值差別不大。

三、記憶體診斷實踐

3.1 記憶體快照生成

當發生線上應用告警,告警相關記憶體故障問題時, 應當如何進行故障排查呢?首先應用在發生記憶體溢位無法執行時,應DUMP當前記憶體快照,需要在Java程式執行啟動命令時新增上:

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=${filePath} 引數

當發生時自動生成一份當前記憶體快照,方便與開發人員使用快照檔案進行問題診斷分析。

在Java應用執行時,想手動生成記憶體快照,可以使用JDK自帶幾個問題排查工具,可以使用jmap工具生成指定PID記憶體快照,不過需要耗費較長的一個時間,會暫停應用程式執行,使用jcmd工具可以快速的DUMP記憶體快照,因為在堆轉儲存檔案過程中,jcmd可以利用虛擬機器中的一些最佳化技術,例如分代堆、增量式垃圾回收等技術,相比傳統的jmap效率高很多,一般來說在DUMP記憶體前會進行一次

Full FC,可以指定遮蔽這次Full GC,保留當前所有記憶體中的物件。

除了自帶的記憶體診斷工具, 也可以使用Arthas診斷工具,提供了多個命令來幫助診斷記憶體問題,例如 dashboard(當前Java程式記憶體實時資料皮膚)、JVM(檢視當前JVM資訊,包括使用的gc收集器、記憶體分割槽分佈情況等資訊)、heapdump(當前記憶體快照類似jmap命令的heap dump)、memory(當前記憶體分割槽及佔用情況)、monitor(監控模式,可監控記憶體及檢視物件佔用情況)profiler(火焰圖可以輸出多種火焰圖,記憶體分割槽佔用火焰圖)等相關記憶體命令。這些命令可以幫助獲取應用程式的記憶體快照、堆記憶體使用情況等資訊,能快速定位記憶體問題。

引用:Arthas 命令列表

3.2 dump記憶體快照分析

(1)jhat 是 Java 開發工具包自帶的一款堆記憶體分析工具,它可以幫助解決 Java 應用程式的記憶體問題。Jhat 可以讀取 Java 應用程式生成的堆轉儲檔案,並以 HTML 格式展示記憶體中的物件資訊和引用關係,支援 OQL 查詢和靈活的過濾和排序功能。

用例 jhat E:\diydump\Java_pid2680.hprof

圖片

  • All classes including platform:列舉應用程式中所有類的資訊,並快速定位記憶體問題。
  • Show all members of the rootset:顯示堆記憶體中所有根物件的資訊,包括系統物件、靜態物件、本地物件等。

  • Show instance counts for all classes (including platform):顯示所有類的例項數量。

  • Show heap histogram:顯示程式堆記憶體的直方圖,可以知道每個類的例項數量和佔用記憶體大小等資訊,快速知道記憶體洩漏原因。

(2)jvisualvm也是Java 開發工具包裡自帶的一款圖形化工具,可以用於監控和診斷Java應用程式的效能問題。使用它可以實時檢視Java 應用程式的記憶體使用情況、CPU使用情況、執行緒情況等,並可以進行記憶體分析、CPU分析、執行緒分析等內容。

以Java_pid2680.hprof為例,進行記憶體分析記憶體洩漏原因:

圖片

(3)MAT 是基於Eclipse的記憶體分析工具,是一個快速、功能豐富的Java記憶體分析工具,能夠快速的分析出dump檔案中各項結果,快速給出記憶體洩漏原因報告。

還是以Java_pid2680.hprof檔案進行分析,比原生的jhat方便很多,功能也比原生的更加豐富:

圖片

MAT的一些常用功能點介紹(如圖所示):

  • Overview 標籤內容有比較多塊內容,其中details末塊介紹總共使用記憶體大小,類的數量,例項的數量,類的載入器,以及例項的記憶體直方圖;

  • Biggest Objects by Retained Size模組,使用了餅狀圖列出了當前記憶體中佔用最大的幾個物件,按照百分比劃分,點選不同的餅狀塊能夠看到具體物件及其物件屬性等資訊;

  • actions模組,這裡擁有不同的分析功能,Histogram生成檢視列出每個類所對應的物件個數以及佔用記憶體大小,Dominator Tree生成檢視尋找出大物件,每個例項物件的記憶體佔比比重;

  • Reports模組是生成報告,其中Leak Suspects可以自動分析記憶體洩漏主要原因報告,可以透過報告準確定位洩漏原因或者可能造成洩漏的原因,並且可以定位到具體累積例項,執行緒stack等資訊。

例子中:leak Suspects報告給出“0xfe3be480” 非常多記憶體, Gc root Thread 所引用,在發生gc時,不是可回收物件,無法回收記憶體,導致記憶體溢位。

圖片

四、總結

本文介紹了Java程式中的記憶體模型,記憶體模型劃分多份記憶體區域,不同區域的作用介紹及不同區域的執行緒之間的記憶體共享範圍,可以幫助開發人員更加理解Java 中記憶體管理的機制和原理。

堆是記憶體模型中最大的一塊記憶體區域,以堆的空間劃分詳細的介紹了記憶體分代,部分垃圾收集器即是物理分代和邏輯分代,G1收集器則物理不分代邏輯保留了以前分代,講述了不同收集器的原理實現和優缺點,可以根據專案的業務屬性,機器配置等因素選擇最優的收集器,幫助程式使用最優的收集器可以使得程式的吞吐量和響應速度達到最佳狀態。還講述了不同的引數調優收集器,並且當發生了程式記憶體溢位崩潰,如何進行記憶體分析,介紹不同工具的使用,快速定位記憶體溢位的罪魁禍首,從而在程式碼層面上根本解決這類問題。

相關文章