JVM+GC 面試題

mortal同學發表於2019-04-23

MinorGC 的過程(複製演算法)

複製->清空->互換

  1. Eden、SurvivorFrom 複製到 SurvivorTo,年齡+1
    • 首先,Eden 區滿的時候回出發第一次 GC,把存活的物件拷貝到 SurvivorFrom 區,當 Eden 區再次出發 GC 的時候會掃描 Eden 區和 From 區,對這兩個區域進行垃圾回收。
    • 經過這次回收後還存活的物件,則直接複製到 To 區,同時將這些物件的年齡+1。如果有物件年齡已經達到了老年的標準,複製到老年代。
  2. 清空 Eden、SurvivorFrom
    • 清空 Eden 和 SurvivorFrom 中的物件
  3. SurvivorTo 和 SurvivorFrom 互換
    • SurvivorTo 和 SurvivorFrom 互換,原來的 SurvivorTo 稱為下一次 GC 時的 SurvivorFrom 區。
    • 部分物件會在 From 和 To 區域中來回複製,如此交換15次(JVM引數 MaxTenuringThreshold決定,預設引數是15),最終如果還是存活,就存入老年代。

GC Roots

垃圾:記憶體中已經不再被使用到的空間就是垃圾。

可達性分析演算法:基本思路是通過一系列名為 GC Roots 的物件作為起始點,從這些物件開始向下搜尋,如果一個物件到 GC Roots 沒有任何引用鏈相連,說明此物件不可用。

GC Roots 是一組必須活躍的引用。

可以作為 GC Roots 的物件的有:

  1. 虛擬機器棧(棧幀中的區域性變數區,也稱區域性變數表)中引用的物件。
  2. 方法區中類靜態屬性引用的物件。
  3. 方法區中常量引用的物件。
  4. 本地方法棧中 JNI ( Native 方法 )引用的物件。

JVM 引數

JVM引數型別

1. 標配引數:在 jdk 各個版本之間保持穩定

- java -version
- java -help
- java -showversion
複製程式碼

2. X引數

-Xint:解釋執行
-Xcomp: 第一次使用就編譯成原生程式碼
-Xmixed: 預設,先編譯後執行
複製程式碼

3. XX引數

  1. Boolean 型別

    • -XX:+/- 屬性
    • +表示開啟,-表示關閉
    • 示例 以是否列印GC收集細節為例:
      • 首先使用 jps -l 檢視當前執行程式 id

      • 然後使用 jinfo -flag PrintGCDetails 程式id來檢視

      • 如果開啟,結果應該是-XX:+PrintGCDetails,否則-XX:-PrintGCDetails

  2. KV型別

    • -XX:key=value
    • 以元空間大小 MetaspaceSize為例:同樣使用上述的方法,使用jinfo -flag MetaspaceSize 程式id來檢視。
    • -Xms等價於-XX:InitialHeapSize-Xmx等價於-XX:MaxHeapSize

如何檢視JVM引數預設值

  1. 使用java -XX:+PrintFlagsInital檢視jvm初始引數
  2. 使用java -XX:+PrintFlagsFinal -version檢視修改更新的引數。引數中使用 = 說明是未修改的引數,使用:=說明是人為或jvm修改過的引數。
  3. 使用java -XX:+PrintCommandLineFlags

JVM常用引數

JDK 1.8 之後永久代取消了,由元空間取代。新生代由 Eden + SurvivorFrom + SurvivorTo 組成,大小用-Xmn設定。JVM 堆由新生代和老年代共同組成,由-Xms-Xmx設定大小。元空間不再屬於堆的一部分。

元空間和永久代的區別在於:永久代使用 JVM 的堆記憶體,元空間不在虛擬機器中而是使用本機實體記憶體

預設情況下,元空間的大小僅受本地記憶體限制,類的後設資料放入 native memory,字串池和類的靜態變數放入 java 堆中,這樣可以載入多少類的後設資料就不再由 MaxPermSize 控制,而由系統的實際可用空間來控制。

  1. -Xms
    • 等價於 -XX:InitialHeapSize
    • 初始大小記憶體,預設為實體記憶體1/64
  2. -Xmx
    • 等價於 -XX:MaxHeapSize
    • 最大分配記憶體,預設為實體記憶體 1/4
  3. -Xss
    • 等價於-XX:ThreadStackSize
    • 設定單個執行緒棧的大小
    • Linux 預設為 1024KB
    • OS X 預設為 1024KB
    • Windows 的預設值取決於虛擬記憶體
  4. -Xmn
    • 設定新生代大小:預設是堆空間1/3
  5. -XX:MetaspaceSize
    • 元空間和永久代類似,都是對方法區的實現。元空間不在虛擬機器中,而是使用本地記憶體。預設情況下,元空間的大小僅受本地記憶體限制。
    • 但是這並不意味元空間不會有OOM,因為其預設記憶體大小是有限的
  6. -XX:+PrintGCDetails
    • 輸出詳細GC手機日誌資訊
[GC [PSYoungGen: 2048K -> 496K(2560K)] 2048K->777K(9728k)]
複製程式碼
  • [GC [PSYoungGen:表示GC型別
  • 2048K表示 YoungGC 前新生代記憶體佔用
  • 496K表示 YoungGC 後新生代記憶體佔用
  • (2560K)表示新生代總大小
  • 2048K 表示 YoungGC 前 JVM 堆記憶體佔用
  • 777K 表示 YoungGC 後 JVM 堆記憶體佔用
  • 9728K 表示 JVM 堆總大小
[Full GC (System) [PSYoungGen:3408K->0K(296688k)] [PSOldGen:0K->3363K(682688K)]3408K->3363K(981376K)[Metaspace:10638K->10638K(131072K)]]
複製程式碼

可以看到 Full GC 中,分別有 新生代、老年代、堆總記憶體以及元空間各自的GC前、GC後以及總大小。

  1. -XX:SurvivorRatio
    • 設定新生代中 Eden 和 S0/S1 的比例
    • 預設為8,表示三者比例為 8:1:1
  2. -XX:NewRatio
    • 設定新生代和老年代在堆中的佔比
    • 設定的值就是設定老年代比新生代的比值
  3. -XX:MaxTenuringThreshold
    • 設定垃圾最大年齡,預設是15
    • java8 中能設定的閾值是 0 ~ 15
    • 設定為較小值,適用於老年代比較多的應用。
    • 設定為較大值,可以增加物件在新生代存活的時間,增加在新生代回收的概率。

四種引用

SoftReference, WeakReference 和 PhantomReference 三個類都繼承自 Reference 類,而 Reference 類又是 Object 類的子類。

強引用

當記憶體不足,JVM 開始垃圾回收,但是強引用的物件,即使出現了 OOM 也不會對該物件進行回收。把一個物件賦給一個引用變數,這個引用變數就是一個強引用,只要還有強引用指向一個物件,該物件就處於可達狀態,不會被 JVM 回收。

軟引用

對於只有軟引用的物件來說,系統充足時不會被回收,系統記憶體不足時會被回收。軟引用通常用在對記憶體敏感的程式中,比如快取記憶體就用到軟引用。

適用場景(快取):假設有一個應用大量讀取本地圖片,每次讀取都會IO影響效能,一次性全部載入到記憶體可能會記憶體溢位。

可以使用 HashMap 儲存圖片的路徑和圖片物件關聯的軟引用之間的對映關係,記憶體不足時, JVM 會自動回收這些快取圖片物件所佔用的空間,有效避免了OOM的問題。

Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();
複製程式碼

弱引用

只要GC一執行,不管JVM的記憶體空間是否足夠,都會回收該物件佔用的記憶體。

WeakHashMap

相比於 HashMap, WeakHashMap 中的元素,當 key 被回收後,Map 中相應的鍵值對將不再存在。

public class WeakHashMapDemo {
    public static void main(String[] args) {
        myHashMap();
        System.out.println("======================");
        myWeakHashMap();
    }

    private static void myWeakHashMap() {
        WeakHashMap<Integer,String> map=new WeakHashMap<>();

        Integer k=new Integer(1);
        String v="str";

        map.put(k,v);
        System.out.println(map);

        k=null;
        System.out.println(map);

        System.gc();
        System.out.println(map);
    }

    private static void myHashMap() {
        HashMap<Integer,String> map=new HashMap<>();

        Integer k=new Integer(1);
        String v="str";

        map.put(k,v);
        System.out.println(map);

        k=null;
        System.out.println(map);

        System.gc();
        System.out.println(map);
    }
}
複製程式碼

執行結果

{1=str}
{1=str}
{1=str}
======================
{1=str}
{1=str}
{}
複製程式碼

引用佇列

public class ReferenceQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        Object o1=new Object();
        ReferenceQueue<Object> referenceQueue=new ReferenceQueue<>();
        WeakReference<Object> weakReference=new WeakReference<>(o1,referenceQueue);
        System.out.println(o1);
        System.out.println(weakReference.get());
        System.out.println(referenceQueue.poll());

        System.out.println();
        o1=null;
        System.gc();
        Thread.sleep(1000);

        System.out.println(o1);
        System.out.println(weakReference.get());
        System.out.println(referenceQueue.poll());

    }
}
複製程式碼
java.lang.Object@1b6d3586
java.lang.Object@1b6d3586
null

null
null
java.lang.ref.WeakReference@4554617c
複製程式碼

弱引用、軟引用、虛引用在 gc 之前,會被放到引用佇列中。

虛引用

虛引用顧名思義就是形同虛設,它並不會決定物件的宣告週期。如果一個物件僅僅持有虛引用,就和沒有引用一樣,隨時可能被回收。虛引用不能單獨使用,也不能通過它訪問物件,必須和引用佇列聯合使用。

虛引用的主要作用是跟蹤物件被垃圾回收的狀態,僅僅是提供了一種確保物件被 finalize 以後,做某些事情的機制。其意義在於說明一個物件已經進入 finalization 階段,可以被回收,用來實現比 finalization 機制更加靈活的回收操作。

設定虛引用的唯一目的,就是在這個物件被回收的時候收到一個系統通知或者後序新增進一步的處理。

public class PhantomReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        Object o1=new Object();
        ReferenceQueue<Object> referenceQueue=new ReferenceQueue<>();
        PhantomReference<Object> phantomReference=new PhantomReference<>(o1,referenceQueue);

        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());

        System.out.println();
        o1=null;
        System.gc();
        Thread.sleep(1000);

        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());
    }
}
複製程式碼

執行結果:

java.lang.Object@1b6d3586
null
null

null
null
java.lang.ref.PhantomReference@4554617c
複製程式碼

可以看到,虛引用的 get() 返回值永遠是 null。

總結

在建立引用的時候可以指定關聯的佇列,當 GC 釋放物件記憶體的時候,會將引用加入到引用佇列。如果程式發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要行動,相當於一種通知機制。通過這種方式,JVM允許我們在物件被銷燬後,做一些我們想做的事情。

OOM

java.lang.StackOverflowError

當遞迴呼叫中,棧的呼叫深度過深,導致棧溢位錯誤。 繼承關係如下:

Throwable -> Error -> VirtualMachineError -> StackoverflowError
複製程式碼

java.lang.OutOfMemoryError: Java heap space

當物件過多,或者存在大物件等情況下,導致堆內容超過堆的最大尺寸,導致堆溢位。

java.lang.OutOfMemoryError: GC overhead limit exceeded

GC回收時間過程會丟擲此類異常。過長的定義是超過 98% 的時間用來做 GC 並且回收了不到 2% 的堆記憶體。連續多次 GC 都只回收了不到 2% 的極端情況下才會丟擲。

假如不丟擲 GC overhead limit 錯誤會發生的情況: GC 清理出的記憶體很快再次被填滿,破事 GC 再次執行,形成惡性迴圈,CPU 使用率一直是 100%,但是 GC 卻沒有任何成果。

public class GCOverheadDemo {
    public static void main(String[] args) {
        int i = 0;
        List<String> list = new ArrayList<>();
        
        while (true) {
            list.add(String.valueOf(++i).intern());
        }
    }
}
複製程式碼

java.lang.OutOfMemoryError: Direct buffer memory

寫 NIO 程式經常使用 ByteBuffer 來讀取或者寫入資料。它可以使用 Native 函式庫直接分配堆外記憶體,然後通過一個儲存在 Java 堆裡邊的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。這樣在一些場景能顯著提高效能,因為避免了在 Java 堆和 Native 堆中來回複製資料。

ByteBuffer.allocate(capability)是分配JVM堆記憶體,屬於GC管轄範圍,由於需要拷貝所以速度相對較慢。

ByteBuffer.allocateDirect(capability)是分配 OS 本地記憶體,不屬於 GC 管轄範圍,由於不需要記憶體拷貝所以速度相對較快。

如果不斷分配本地記憶體,堆記憶體很少使用,那麼 JVM 就不需要執行 GC,DirectByteBuffer 物件就不會被回收,導致堆記憶體充足,但是本地記憶體已經被用光了。

java.lang.OutOfMemoryError: Unable to create new native thread

在高併發請求伺服器時,會出現該異常。

導致的原因:

  1. 應用建立了太多的執行緒,一個應用程式建立多個執行緒,超過了系統承載的極限
  2. 伺服器不允許應用程式建立這麼多執行緒,linux 系統預設單個程式可以建立的執行緒數是 1024。

解決辦法:

  1. 想辦法降低建立執行緒的數量
  2. 確實需要建立很多執行緒的應用,可以修改linux伺服器配置,擴大其限制

java.lang.OutOfMemoryError: metaspace

MetaSpace 是方法區在 HotSpot 中的實現,它並不在虛擬機器記憶體中而是使用本地記憶體,也就是類的後設資料被儲存在 MetaSpace 的 native memory 中。

其中存放了:虛擬機器載入的類的資訊,常量池,靜態變數和即時編譯的程式碼。

垃圾回收器

垃圾回收器的四種思想

Serial

序列垃圾回收器,為單執行緒環境設計且只使用一個執行緒進行垃圾回收,會暫停所有的使用者執行緒,所以不適合伺服器環境。

過程就是:程式執行->單GC執行緒->程式執行

Parallel

並行垃圾回收器,多個垃圾收集執行緒並行工作,此時使用者執行緒是暫停的,適用於科學計算、大資料處理等弱互動場景。

過程:程式執行->多GC執行緒並行執行->程式執行

CMS

併發垃圾回收器,使用者執行緒和GC執行緒同時執行,不需要停頓使用者執行緒,適用於對響應時間有要求的場景(強互動)。

過程:初始標記(暫停,單執行緒)->併發標記(GC執行緒和使用者執行緒同時執行)->最終標記(暫停,多GC執行緒)->清除

G1

G1垃圾回收器將堆記憶體分割成不同的區域(Region)然後併發的對其進行垃圾回收。

檢視預設的垃圾回收器

使用java -XX:+PrintCommandLineFlags -version檢視預設的垃圾回收器。

在 java8 中,預設的是 -XX:+UseParallelGC

預設垃圾回收器有哪些

java 的 gc 型別有以下幾種:

  • UseSerialGC
  • UseParallelGC
  • UseConcMarkSweepGC
  • UseParNewGC
  • UseParallelOldGC
  • UseG1GC

在 jvm 中的 7 種垃圾回收器中,Serial Old 已經被廢棄,所以在 gc 的原始碼中,有如上六種。

七種垃圾回收器

在新生代的有:

  1. Serial
  2. Parallel Scavenge
  3. ParNew

在老年代的有:

  1. Serial Old
  2. Parallel Compacting
  3. CMS

二者都可以使用的是 G1。

JVM+GC 面試題

DefNew: Default New Generation
Tenured: Old
ParNew: Parallel New Generation
PSYoungGen: Parallel Scavenge
ParOldGen: Parallel Old Generation
複製程式碼

新生代序列收集器: Serial

一個單執行緒的收集器,在GC的時候,必須暫停其他所有的工作執行緒直到 GC 結束。

JVM+GC 面試題
在單CPU環境下,沒有執行緒互動的開銷可以獲得最高的GC效率,因此 Serial 垃圾收集器是 JVM 在 Client 模式下預設的新生代垃圾收集器。

JVM引數:-XX:+UseSerialGC 開啟後,新生代使用 Serial 老年代使用 Serial Old,新生代和老年代都使用序列回收收集器,新生代使用複製演算法,老年代使用標記-整理演算法。

GC 日誌中,新生代使用DefNew,老年代使用Tenured

新生代並行收集器: ParNew

使用多執行緒進行垃圾回收,垃圾收集時,會 STW 暫停其他執行緒直到 GC 結束。

JVM+GC 面試題
ParNew 收集器是 Serial 的並行多執行緒版本,常見的應用場景是配合老年代的 CMS 工作。是很多 jvm 執行在 Server 模式下的新生代的預設垃圾收集器

JVM 引數:-XX:+UseParNewGC。啟用後,新生代使用 ParNew ,老年代使用 Serial Old,新生代使用複製演算法,老年代使用標記-整理演算法。 要注意的是, ParNew + Serial Old 已經不再被推薦。

GC 日誌中,新生代使用ParNew,老年代使用Tenured

新生代並行收集器: Parallel Scavenge

Parallel Scavenge 收集器類似於 ParNew 也是一個新生代垃圾收集器,使用複製演算法,是並行的多執行緒的垃圾收集器,是吞吐量優先的收集器。

它重點關注的是吞吐量(執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間))。高吞吐量意味著高效利用CPU的時間,適用於在後臺運算而不需要太多互動的任務

Parallel Scavenge 和 ParNew 收集器的一個重要區別是自適應調節策略。自適應調節策略是指虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或最大的吞吐量。

JVM 引數:-XX:+UseParallelGC,與使用-XX:+UseParallelOldGC效果相同,這兩個收集器預設搭配使用,所以會互相啟用。開啟後,新生代使用複製演算法,老年代使用標記-整理演算法。

啟用該收集器後,GC 日誌中,新生代輸出PSYoungGen,老年代輸出ParOldGen

在 Java 8 中,該收集器為預設的收集器。

老年代並行收集器 : Parallel Old

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,使用多執行緒的標記-整理演算法,在 JDK 1.6開始提供。

在 JDK 1.6 之前,新生代使用 Parallel Scavenge 只能搭配老年代的 Serial Old,只能保證新生代的吞吐量優先。

在 JDK 1.8 及以後,預設搭配為 Parallel Scavenge + Parallel Old ,保證了整體的吞吐量優先。

JVM 引數:-XX:+UseParallelOldGC ,開始後,新生代使用 Parallel Scavenge,老年代使用 Parallel Old。

啟用該收集器後,GC 日誌中,新生代輸出PSYoungGen,老年代輸出ParOldGen

老年代併發標記清除收集器:CMS

CMS(Councurrent Mark Sweep)是一種以獲取最短回收停頓時間為目標的收集器,適合應用在網際網路或B/S系統的伺服器上,這類應用中是伺服器的相應速度,希望系統停頓時間最短。CMS 適合堆記憶體大、CPU 核數多的伺服器端應用。

JVM 引數:-XX:+UseConcMarkSweepGC,開啟該引數後會自動開啟-XX:+UseParNewGC。使用 ParNew (新生代) + CMS + Serail Old 的收集器組合,其中 Serial Old 作為 CMS 出錯的後備收集器。

JVM+GC 面試題

  1. 初始標記:標記 GC Roots 直接關聯的物件,速度很快,需要 STW。
  2. 併發標記:進行 GC Roots 跟蹤的過程,和使用者執行緒一起工作,不需要暫停工作執行緒,標記全部物件。
  3. 重新標記:修正在併發標記期間,因為使用者程式繼續執行導致標記產生變動的物件的記錄,需要STW。
  4. 併發清除:清除 GC Roots 不可達物件,和使用者執行緒一起工作,不需要暫停工作執行緒。

由於耗時最長的併發標記和併發清除過程中,GC 執行緒和使用者一起併發工作,所以總體上看 CMS 的 GC 和使用者執行緒一起併發執行。

優點:併發收集、停頓低。

缺點:

  • 併發執行,對CPU資源壓力大: 併發執行, CMS 在收集時會增加對堆記憶體的佔用。因此 CMS 必須要在老年代堆記憶體用盡之前完成垃圾回收,否則 CMS 回收失敗時,將觸發擔保機制, Serial Old 會以 STW 的方式進行 GC ,造成較大停頓時間
  • 使用標記清除演算法導致大量碎片

老年代序列收集器:Serial Old

Serial Old 是 Serial 收集器的老年代版本,是個單執行緒收集器,使用標記-整理演算法,是 Client 預設的老年代收集器。

在 Server 模式下:

  • 在 JDK 1.5 之前與新生代 Parallel Scavenge 搭配。
  • 作為老年代使用 CMS 收集器的後備垃圾收集方案。

如何選擇垃圾收集器

  • 單CPU或小記憶體、單機程式:-XX:+UseSerailGC
  • 多CPU,需要最大吞吐量,如後臺計算型應用:-XX:+UseParallelGC-XX:+UseParallelOldGC
  • 多CPU,追求低停頓時間,需要快速響應的應用:-XX:+UseConcMarkSweepGC
引數 新生代垃圾收集器 新生代演算法 老年代垃圾收集器 老年代演算法
-XX:+UseSerialGC SerialGC 複製 SerailOldGC 標記整理
-XX:+UseParNewGC ParNew 複製 SerailOldGC 標記整理
-XX:+UseParallelGC/-XX:+UseParallelOldGC Parallel Scavenge 複製 Parallel Old 標記整理
-XX:+UseConcMarkSweepGC ParNew 複製 CMS+Serial Old 標記清除

G1 收集器

garbage-first heap+metaspace

G1以前收集器的特點

  1. 年輕代和老年代是各自獨立且連續的記憶體塊
  2. 年輕代使用 eden + s0 + s1 的複製演算法
  3. 老年代收集必須掃描整個老年代區域
  4. 都以儘可能少而快速執行 GC 為設計原則

G1 介紹

G1 是一種伺服器端的垃圾收集器,應用在多處理器和大容量記憶體環境中,在實現高吞吐量的同時,儘可能的滿足垃圾收集暫停時間的要求。並且具有如下特性:

  • 像 CMS 收集器一樣,能與應用程式執行緒併發執行
  • 整理空閒空間更快
  • 需要更多的時間預測 GC 停頓時間
  • 不希望犧牲大量的吞吐效能
  • 不需要更大的 Java Heap

G1 的設計目標是取代 CMS 收集器,同 CMS 相比:

  • G1 有整理記憶體過程,不會產生很多記憶體碎片
  • G1 的 STW 更可控,在停頓時間上新增了預測機制,使用者可以指定期望停頓時間

G1 的主要改變是 Eden, Survivor 和 Tenured 等記憶體區域不再是連續的,而是變成了一個個大小一樣的 region。

G1 的特點

  1. G1 能充分利用多 CPU 的硬體優勢,儘量縮短 STW
  2. G1 整體採用標記整理演算法,區域性通過複製演算法,不會產生記憶體碎片
  3. G1 把記憶體劃分為多個獨立的 Region
  4. G1 邏輯上保留了新生代和老年代,但是不再是物理隔離的,而是一部分 Region 的集合(並且不要求 Region 是連續的),會採用不同的 GC 方式處理不同的區域。
  5. G1 只有邏輯上的分代概念,每個分割槽都可能隨著 G1 的執行在不同代之間切換。

Region

JVM+GC 面試題
將堆劃分成了 Region,避免了全記憶體區域的 GC 操作。G1 並不要求物件的儲存是物理上連續的,只需要邏輯上連續即可,每個分割槽可以按需在新生代和老年代之間切換。

每個 Region 大小範圍在 1MB-32MB,最多可以設定 2048 個區域(預設也是2048),因此能支援的最大記憶體為 64 GB。

G1 仍然屬於分代收集器。這些 Region 的一部分包含新生代,新生代的垃圾(Eden)收集採用 STW 的方式,將存活物件拷貝到老年代或者 Survivor 空間。Region 的另一部分屬於老年代, G1 通過將物件從一個區域複製到另外一個區域,完成清理工作,因此不會有 CMS 記憶體碎片問題的存在了。

在 G1 中,還有一種特殊的區域,稱為 Humongous 區域,如果一個物件佔用的空間超過了 Region 的 50% 以上,就被認為是巨型物件,會直接分配在老年代。G1 劃分了一個 Humongous 區,用來存放巨型物件。如果一個 Humongous Region 裝不下一個巨型物件,G1會尋找連續的 Humongous Region 來儲存,為了能找到連續的 H 區,有時候不得不啟動 Full GC。

回收步驟

Young GC: 針對 Eden 區進行收集, Eden 區耗盡後被處罰,主要是小區域收集 + 形成連續的記憶體塊,避免記憶體碎片。

  • Eden 區的資料移動到 Survivor 區,如果 Survivor 區空間不夠,晉升到 Old 區
  • Survivor 區資料移動到新的 Survivor 區,部分晉升到 Old 區
  • GC 結束,應用程式繼續執行

JVM+GC 面試題
步驟:

  1. 初始標記
  2. 併發標記
  3. 最終標記
  4. 篩選回收:根據時間來進行價值最大化的回收

微服務生產部署和調參優化

生產環境伺服器變慢

  1. 檢視整機情況
    • 使用 top 命令檢視CPU 佔用率、記憶體佔用率以及第一行的 load average
    • load average 後的三個數字分別記錄了一分鐘、五分鐘、以及十五分鐘的系統平均負載
    • 還可以使用 uptime命令,是系統效能命令的精簡版
  2. 檢視 CPU 情況
    • 使用vmstat -n 2 3代表每2秒取樣一次,一共取樣3次
    • procs 中
      • r代表執行和等待 CPU 時間片的程式數,整個系統的執行佇列不應超過總核數的2倍,否則代表系統壓力過大
      • b代表等待資源如磁碟 I/O,網路 I/O 的程式數
    • cpu 中
      • us 代表使用者程式消耗 CPU 時間百分比,如果長期大於 50% 要優化程式
      • sy 代表核心程式消耗 CPU 時間百分比
      • us + sy 參考值為 80% ,如果和大於 80% ,可能存在 CPU 不足
    • 使用mpstat -P ALL 2檢視所有 CPU 核資訊
    • 使用pidstat -u 1 -p pid檢視每個程式使用 CPU 的用量分解資訊
  3. 檢視記憶體資訊
    • 使用free -m來檢視記憶體情況,單位為 mb
    • 應用程式可用記憶體/系統實體記憶體 應該在 20%~70%
    • 使用pidstat -p pid -r 取樣間隔秒數
  4. 檢視硬碟資訊
    • 使用df -h
  5. 檢視磁碟IO
    • 使用iostat -xdk 2 3
      • rkB/s 每秒讀取資料量
      • wkB/s 每秒寫入資料量
      • svctm I/O請求的平均服務時間,單位為ms
      • util 一秒有百分之多少的時間用於 I/O 操作
    • 使用pidstat -d 取樣間隔秒 -p pid
  6. 檢視網路IO
    • 使用 ifstat 取樣間隔秒

CPU 佔用過高的分析

  1. 使用 top 命令找出 CPU 佔比最高的程式
  2. ps -ef 或者 jps 進一步定位該程式資訊
  3. 定位到具體執行緒或程式碼
    • 使用ps -mp pid -o THREAD,tid,time
      • -m 顯示所有執行緒
      • -p pid 程式使用 cpu的時間
      • -o 後是使用者自定義格式
  4. 將需要的執行緒 id 轉換為 16 進位制(英文小寫格式)
  5. jstack 程式id | grep tid -A60

相關文章