一文理清JVM和GC 【第二彈】

1、OOM的認識

StackOverflowError

 public static void main(String[] args) {
     stackOverflowError();   //Exception in thread "main" java.lang.StackOverflowError
 }
private static void stackOverflowError() {
    stackOverflowError();
}

OutOfMemeoryError:java heap space

public static void main(String[] args) {
    String str = "cbuc";
    for (; ; ) {
        str += str + UUID.randomUUID().toString().substring(0,5);   //+= 不斷建立對象
    }
}

OutOfMemeoryError:GC overhead limit exceeded

程序在垃圾回收上花費了98%的時間,卻收集不會2%的空間。
假如不拋出GC overhead limit,會形成:java

  • GC清理的一點點內存很快會再次填滿,迫使GC再次執行,這樣就造成了惡性循環。linux

  • CPU的使用率一直是100%,而GC卻沒有任何成果ios

圖片

OutOfMemeoryError:Direct buffer memory

  • 寫NIO程序常用 ByteBuffer 來讀取或者寫入數據,這是一種基於通道(Channel)和緩衝區(Buffer)的 I/O 方式,它可使用Native 函數庫直接分配堆外內存,而後經過一個存儲在Java 堆裏面的DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。算法

ByteBuffer.allocate(capability):這一種方式是分配JVM堆內存,屬於GC管轄範圍,因爲須要拷貝因此速度相對較慢。服務器

ByteBuffer.allocateDirect(capability):這一種方式是分配OS本地內存,不屬於GC管轄範圍,因爲不須要內存拷貝,因此速度相對較快。網絡

可是若是不斷分配本地內存,堆內存不多使用,那麼JVM就不須要執行GCDirectByteBuffer 對象就不會被回收,這時候堆內存充足,但本地內存可能就已經使用光了,再次嘗試分配本地內存就會出現OutOfMemeoryError,那程序就直接奔潰了。多線程

public static void main(String[] args) {
    /**
     * 虛擬機配置參數
     * -Xms10m -Xmx10m -XX:+PrintGCDetails  -XX:MaxDirectMemorySize=5m
     */

    System.out.println("配置的maxDirectMemeory:"+     (sun.misc.VM.maxDirectMemory()/(double)1024/1024)+"MB");
    try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
    // -XX:MaxDerectMemorySize=5m  配置爲5m, 這個時候咱們使用6m
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6*1024*1024);

}
  • OutOfMemeoryError:unable to create new native thread併發

高併發請求服務器時,常常會出現該異常
致使緣由dom

  1. 你的應用建立了太多線程了,一個應用進程建立多個線程,超過系統承載權限。ide

  2. 你的服務器並不容許你的應用程序建立這麼多線程,linux系統默認容許的那個進程能夠建立的線程數是1024個,你的應用建立超過這個數量就會報OutOfMemeoryError:unable to create new native thread

解決辦法

  1. 想方法減低你應用程序建立線程的數量,分析應用是否真的須要建立那麼多線程,若是不是,改代碼將線程數降到最低。

  2. 對於有點應用,確實須要建立不少線程,遠超過linux系統默認1024個線程的限制,能夠經過修改linux服務器配置,擴大linux默認限制

public static void main(String[] args) {
        for (int i = 1;  ; i++) {
            System.out.println("輸出 i: " + i);
             new Thread(()->{
                 try {TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {e.printStackTrace();}
             },"線程"+i).start();
        }
    }

OutOfMemeoryError:Metaspace

Java 8以後的版本使用Metaspace來替代永久代
Metaspace是方法區在HotSpot中的實現,它與持久帶最大的區別在於:Metespace並不在虛擬機內存中而是使用本地內存
永久代(java8 後被原空間Metaspace取代了)存放了如下信息:

  • 虛擬機加載的類信息

  • 常量池

  • 靜態常量

  • 即時編譯後的代碼

2、4種垃圾收集器

GC算法(引用計數/複製/標清/標整)是內存回收的方法,垃圾收集器就是算法的實現

目前爲止尚未完美的收集器出現,更加沒有萬能的收集器,只是針對具體應用最合適的收集器,進行分代收集

串行垃圾回收器(Serial)

它爲單線程環境設計而且只是用一個線程進行垃圾回收,會暫停全部的用戶線程。因此不適合服務器環境。

並行垃圾回收器(parallel)

多個垃圾回收線程並行工做,此時用戶線程是暫停的,適用於科學計算/大數據處理等弱交互場景

併發垃圾回收器(CMS)

用戶線程和垃圾收集線程同時執行(不必定是並行,可能交替執行),不須要停頓用戶線程,適用於對響應時間有要求的場景

G1垃圾回收器

G1垃圾回收器將堆內存分割成不一樣的區域而後併發的對其進行垃圾回收

3、垃圾收集器解析

查看默認的垃圾收集器

java -XX:+PrintCommandLineFlags -version

圖片

默認的垃圾收集器

  • UseSerialGC

  • UseParallelGC

  • UseConcMarkSweepGC

  • UseParNewGC

  • UseParallelOldGC

  • UseG1GC

新生代

  • 串行GC(Serial)/(Serial Coping)
    一個單線程的收集器,在進行垃圾收集的時候,必須暫停其餘全部的工做線程知道它收集結束

    圖片

最穩定以及效率高的收集器,只使用一個線程去回收但其在進行垃圾手機過程當中可能會產生較長的停頓(「Stop-The-World」狀態)。雖然在收集垃圾過程當中須要暫停全部其餘的工做線程,可是它簡單高效,對於限定單個CPU環境來講,==沒有線程交互的開銷能夠得到更高的單線程垃圾收集效率,== 所以Serial垃圾收集器依然是Java虛擬機運行在Client 模式下默認的新生代垃圾收集器。

JVM設置參數
-XX:+UseSerialGC開啓後會使用:Serial(Young區用)+Serial Old(Old區用的)收集器組合
表示

新生代、老年代都會使用串行回收收集器,新生代使用複製算法,老年代使用標記-整理算法

  • 並行GC(ParNew)

使用多線程進行垃圾回收,在垃圾收集時,會Stop-The-World暫停其餘全部工做的線程知道它收集結束

圖片

ParNew收集器其實就是Serial收集器新生代的並行多線程版本,最多見的應用場景是配合老年代的CMS GC工做,其他的行爲和Serial收集器徹底同樣,ParNew垃圾收集器在垃圾收集過程當中一樣也要暫停全部的工做線程。它是不少java虛擬機運行在Server模式下新生代的默認垃圾收集器。

JVM設置參數

XX:+UseParNewGC啓用 ParNew收集器,隻影響新生代的收集,不影響老年代。開啓上述參數後,會使用:ParNew (新生代區用)+Serial Old(老年代區用)策略新生代使用複製算法,老年代使用標記-整理算法

  • 並行回收GC(Parallel)/(Parallel Scavenge)

    圖片

    Parallel Scavenge收集器相似ParNew 也是新生代垃圾收集器,使用複製算法,也是一個並行的多線程的垃圾收集器,俗稱吞吐量優先收集器。串行收集器在新生代和老年代的並行化

    關注點:

  1. 可控制的吞吐量

  2. 自適應調節策略也是ParallelScavenge收集器與ParallelNew收集器的一個重要區別

    JVM設置參數:
    -XX:UseParallelGC 或 -XX:UseParallelOldGC(可互相激活),開啓後:新生代使用複製算法,老年代使用標記-整理算法

老年代

  • 串行GC(Serial Old)/(Serial MSC)
    Serial Old 是Serial 垃圾收集器老年代版本,它一樣是個單線程的收集器,使用標記-整理算法,這個收集器也主要是運行在Client默認的java虛擬機默認的老年代垃圾收集器。
    用途

  1. 在JDK1.5以前版本中與新生代的Parallel Scavenge收集器搭配使用。(Parallel Scavenge+Serial Old

  2. 做爲老年代版中使用CMS收集器的後備垃圾收集方案。

  • 並行GC(Parallel Old)/(Parallel MSC)
    Parallel Old收集器是Parallel Scavenge的老年代版本,使用多線程的標記-整理算法,Parallel Old在JDK 1.6以前,新生代使用 ParallelScavenge 收集器,只能保證新生代的吞吐量優先,沒法保證總體的吞吐量。在JDK1.6以前(Parallel Scavenge+Serial Old
    Parallel Old 正是爲了在年老代一樣提供吞吐量優先的垃圾收集器,若是系統對吞吐量要求比較高,JDK1.8 後能夠優先考慮新生代Parallel Scavenge和年老代 Parallel Old收集器的搭配策略。

    JVM設置參數
    -XX:+UseParallelOldGC開啓 Parallel Old收集器,設置該參數後,使用 新生代Parallel + 老年代Parallel Old策略

  • 併發標記清除GC(CMS)

    優勢

    併發收集低停頓
    缺點

  • 併發執行,對CPU資源壓力大
    因爲併發進行,CMS在收集與應用線程會同時會增長對堆內存的佔用,也就是說,CMS必需要在老年代堆內存用盡以前完成垃圾回收,不然CMS回收失敗時,將觸發擔保機制,串行老年代收集器將會以STW的方式進行一次GC,從而形成較大停頓時間。

  • 採用的標記清除算法會致使大量碎片
    標記清除算法沒法整理空間碎片,老年代空間會隨着應用時長被逐步耗盡,隨後將不得不經過擔保機制對堆內存進行壓縮。CMS也提供了參數-XX:CMSFulllGCsBeForeCompaction(默認0,即每次都進行內存整理)來指定多少次CMS收集以後,進行一次壓縮的Full GC。

    關鍵4步

  1. Initial Mark (初始標記):標記GC Root能夠直達的對象,耗時短。

  2. Concurrent Mark(並行標記):從第一步標記的對象出發,併發地標記可達對象。

  3. Remark(從新標記):從新進行標記,修正Concurrent Mark期間因爲用戶程序運行而致使對象間的變化及新建立的對象,耗時短。

  4. Concurrent Sweep(並行回收):並行地進行無用對象的回收。

    圖片

如何選擇垃圾收集器

  • 單CPU或小內存,單機程序
    -XX:+UseSerialGC

  • 多CPU,須要最大吞吐量,如後臺計算型應用
    -XX:+UseParallelGC
    -XX:+UseParallelOldGC

  • 多CPU,追求低停頓時間,需快速響應如互聯網應用
    -XX:+UseConcMarkSweepGC
    -XX:+ParNewGC

    圖片

4、G1垃圾收集器

之前垃圾收集器的特色

  1. 年輕代和老年代是各自獨立且連續的內存塊

  2. 年輕代中Eden+S0+S1使用複製算法進行收集

  3. 老年代收集必須掃描整個老年代區域

  4. 都是以儘量少而快速地執行GC爲設計原則

G1 概念:

Garbage-First收集器,是一款面向服務端應用的收集器,優勢以下:

  • 整理空閒空間更快

  • 須要更多的時間來預測GC停頓時間

  • 不但願犧牲大量的吞吐性能

  • 不須要更大的Java Heap

G1收集器的設計目標是取代CMS收集器

G1 優點:

  1. G1 是一個有整理內存過程的垃圾收集器,不會產生不少內存碎片

  2. G1 的Stop-The-World (STW)更可控,G1在停頓時間上添加了預測機制,用戶能夠指按期望停頓時間

主要改變是EdenSurvivorTenured等內存區域再也不是連續的了,而是變成了一個個大小同樣的region,每一個region1M32M不等。一個region有可能屬於EdenSurvivor或者Tenured內存區域。

G1特色:

  • G1能充分利用多CPU,多核環境硬件優點,儘可能縮短STW

  • G1總體上採用標記-整理算法,局部是經過複製算法,不會產生內存碎片

  • 宏觀上看G1之中再也不區分年輕代和老年代。把內存劃分紅多個獨立的子區域(Region)

  • G1收集器裏面講整個的內存區都混合在一塊兒了,但其自己依然在小範圍內要進行年輕代和老年代的區分,保留了新生代和老年代。

  • G1雖然也是分代收集器,但整個內存分區不存在物理上的年輕代與老年代的區別,也不須要徹底獨立的survivor(to space)堆作複製準備。G1只有邏輯上的分代概念,或者說每一個分區均可能隨G1的運行在不一樣代之間先後切換。

G1底層原理

(1)Region區域化垃圾收集器·

區域化內存劃片Region,總體編爲了一下列不連續的內存區域,避免了全內存區的GC操做。
核心思想

將整個堆內存區域分紅大小相同的子區域(Region),在JVM啓動時會自動配置這些子區域的大小
在堆的使用上,G1並不要求對象的存儲必定是物理上連續的只要邏輯上連續便可,每一個分區也不會固定地爲某個代服務,能夠按需在年輕代和老年代之間切換。啓動時能夠經過參數-XX:G1HeapRegionSize=n 可指定分區大小(1MB~32MB,且必須是2的冪),默認將整堆劃分爲2048個分區。
大小範圍在1MB~32MB,最多能設置2048個區域,也即可以支持的最大內存爲:32MB*2048=65536MV=64G內存
最大好處就是化整爲零,避免全內存掃描,只須要按照區域來進行掃描便可


(2)回收步驟

針對Eden區進行收集,Eden區耗盡後會被觸發,主要是小區域收集+造成連續的內存塊,避免內存碎片

  • Eden區的數據移動到新的Survivor區,部分數據晉升到Old區。

  • Survivor區的數據移動到新的Survivor區,部分數據晉升到Old區。

  • 最後Eden區收拾乾淨了,GC結束,用戶的應用程序繼續執行。

    圖片
(3)執行四步
  • 初始標記

    只標記GC Roots能直接關聯到的對象

  • 併發標記

    進行GC Roots Tracing的過程

  • 最終標記

    修正併發標記期間,因程序運行致使標記發生變化的那一部分對象

  • 篩選回收

    根據時間來進行價值最大化的回收

    圖片
(4)經常使用配置參數
  • -XX:+UseG1GC
    開啓G1垃圾收集器

  • -XX:G1HeapRegionSize=n
    設置G1區域的大小。值是2的冪,範圍是1M到32M。目標是根據最小的Java堆大小劃分出約2048個區域

  • -XX:MaxGCPauseMillis=n
    最大停頓時間,這是個軟目標,JVM將盡量(但不保證)停頓時間小於這個時間

  • -XX:InitiatingHeapOccupancyPercent=n
    堆佔用了多少的時候就觸發GC,默認是45

  • -XX:ConcGCThreads=n
    併發GC使用的線程數

  • -XX:G1ReservePercent=n
    設置做爲空閒時間的預留內存百分比,以下降目標空間溢出的風險,默認值是10%

(5)與CMS相比的優點
  • G1不會產生內存碎片

  • 是能夠精確控制停頓,該收集器是把整個堆(新生代、老年代)劃分紅多個固定大小的區域,每次根據容許停頓的時間去收集垃圾最多的區域。

(6)總結

圖片

5、診斷生產環境服務器變慢

整機相關

top

圖片
前五行是統計信息
第一行是任務隊列信息,同uptime命令的執行結果同樣
17:16:47:當前時間
up 23:47:系統運行時間
2 users:當前登陸用戶數
load average:0.21,0.27,0.19:系統負載,既任務隊列的平均長度,三個數值分別爲1分鐘、5分鐘、15分鐘前到如今的平均值

CPU相關

1)vmstat

圖片
vmstat -n 2 3
第一個參數是採樣的時間間隔數(單位:秒),第二個參數是採樣的次數
主要參數

  • procs
    r: 運行和等待CPU時間片的進程數,原則上1核的CPU的運行隊列不要超過2,整個系統的運行隊列不能超過總核數的2倍,不然表明系統壓力過大。
    b: 等待資源的進程數,好比正在等待磁盤I/O,網絡I/O等。

  • cpu
    us:用戶進程消耗CPU時間百分比,us值高,用戶進程消耗CPU時間多,若是長期大於50%,須要優化程序
    sy: 內核進程消耗的CPU時間百分比
    us + sy 參考值爲80%,若是us + sy 大於80%,說明可能存在CPU不足
    id: 處於空閒CPU百分比
    wa: 系統等待IO的CPU時間百分比
    sy: 來自於一個虛擬機偷取的CPU時間的百分比

2)mpstat

mpstat -P ALL 2
查看CPU核信息

圖片

3)pidstat

pidstat -u 1 -p 進程號
每一個進程使用cpu的用量分解信息

內存相關

free

應用程序中可用內存 / 系統物理內存>70%內存充足
應用程序可用內存/系統物理內存<20% 內存不足須要增長內存
20%<應用程序可用內存/系統物理內存<70%內存基本夠用

圖片

硬盤相關

df

查看磁盤剩餘空閒數

圖片

硬盤IO相關

iostat -xdk 2 3

6、分析生產環境CPU佔用太高

步驟1

先用top命令找出CPU佔比最高的

步驟2

ps -ef 或者 jps 進一步定位,得知是一個怎樣的後臺程序

步驟3

定位到具體線程或者代碼
ps -mp 進程 -o THREAD,tid,time

-o:該參數是用戶自定義格式
-p:pid進程使用cpu的時間
-m: 顯示全部線程

步驟4

將須要的線程ID轉換爲16進制格式(英文小寫格式)
再使用:printf "%x/\n" 有問題的線程ID

步驟5:

jstat 進程ID | grep tid(16進制線程ID小寫英文)

7、經常使用的JVM監控和性能分析工具

  • jps

    虛擬機進程情況工具

  • jinfo

    Java配置信息工具

  • jmap

    內存映像工具

  • jstat

    統計信息監控工具

相關文章
相關標籤/搜索