JVM 故障調查教程

JVM 故障調查教程

本文主要針對java應用程序佔用CPU資源過高問題進行的調查分析以及總結,CPU佔用過高的主要原因是jvm的內存不足引用的GC瘋狂回收空間,導致GC的回收線程把CPU資源用盡。內存不足的原因主要有。1.業務預估不合理導致請求處理暴增。2程序代碼邏輯問題導致(死循環,內存異常等)。下面通過代碼示例分析證明問題的原因,以及解決思路,提供詳細的解決方法,工具介紹,經驗介紹等。希望本文爲讀者解決項目中JVM相關問題提供幫助。

java 程序 cpu100%原因排查

模擬cpu佔用100%代碼示例

public class App {
public static void main(String[] args) throws InterruptedException {
int num = 20;
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
threads[i] = new Thread(new PressureRunner());
threads[i].start();

}


}

public static class PressureRunner implements Runnable {
@Override
public void run() {
while (true) {

}
}
}

}

編譯:javac App.java

執行:java App.java

第一步:確認cpu佔用情況及進程ID

top

 

第二步: 顯示進程下的線程佔用CPU情況

top -H -p pid

找到佔用CPU最高的線程

 

 

 

echo "obase=16;9758" | bc

線程id結果是

261E

 

第三步: 導出java進程的線程棧

jstack pid > pid.tdump

執行

jstack 9745 > 9745.tdump

#
查看tdump
cat 9745.tdump

代碼定位

 

小結

本例子是針對業務程序引用的CPU100%的分析方法,還可能會有,併發io讀寫或網絡讀取而引起的內存資源用完引起的CPU佔用100%的情況。也適用此方法來定位問題代碼。一般這種情況如果計算任務釋放或io讀寫或網絡資源釋放,cpu佔用情況自動會下降,不會導致jvm奔潰。 還有另外一種引起的CPU佔用100%情況,就是JVM空間不夠引起的GC頻繁回收。GC是使用多線程回收的。導致大量線程瘋狂回收最終CPU資源佔滿。而且是不會釋放的,CPU一直100%。解決辦法就要調整jvm的增加內存配置。請看下節。

jvm 內存空間不足引起的CPU100% 原因排除

代碼

package com.hdk.demofullgc;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
* VM
參數: -XX:+PrintGC -Xms50M -Xmx50M
* GC
調優---生產服務器推薦開啓(默認是關閉的)
* -XX:+HeapDumpOnOutOfMemoryError
*/

public class FullGCProblem {
//
線程池

private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
new ThreadPoolExecutor.DiscardOldestPolicy());

public static void main(String[] args) throws Exception {
//50
個線程
executor.setMaximumPoolSize(50);
while (true){
calc();
Thread.sleep(100);
}
}

/**
*
多線程執行任務計算
*/
private static void calc(){
List<UserInfo> taskList = getAllCardInfo();
taskList.forEach(userInfo -> {
executor.scheduleWithFixedDelay(() -> {
userInfo.user();
}, 2, 3, TimeUnit.SECONDS);
});
}

/**
*
模擬從數據庫讀取數據,返回
* @return
*/
private static List<UserInfo> getAllCardInfo(){
List<UserInfo> taskList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
UserInfo userInfo = new UserInfo();
taskList.add(userInfo);
}
return taskList;
}
private static class UserInfo {
String name = "hdk";
int age = 18;
BigDecimal money = new BigDecimal(999999.99);

public void user() {
//
}
}
}

 

運行代碼:

java -cp demofullgc.jar -XX:+PrintGC -Xms50M -Xmx50M com.hdk.demofullgc.FullGCProblem 結果

GC 日誌詳解

 

GC 常用參數

首先JVM內存限制於實際的最大物理內存,假設物理內存無限大的話,JVM內存的最大值跟操作系統有很大的關係。簡單的說就32位處理器雖然可控內存空間有4GB,但是具體的操作系統會給一個限制, 這個限制一般是2GB-3GB(一般來說Windows系統下爲1.5G-2G,Linux系統下爲2G-3G),而64bit以上的處理器就不會有限制了

運行:top 查看CPU佔用情況及pid

 

Jstat

代碼中有打印 GC 參數,生產上可以使用這個 jstat –gc 來統計

 

 

分析:jstat-gc 5953 2500 100 需要每 250 毫秒查詢一次進程 5953 垃圾收集狀況,一共查詢 100 次。

隨着GCT和GCT逐漸變大,FGC(老年代垃圾回收次數)頻率也越來越高。1,1,2,2,3,3,5,6,245。

最後拋出outofmemoryerror異常。CPU佔用100%

S0C:第一個倖存區的大小

S1C:第二個倖存區的大小

S0U:第一個倖存區的使用大小

S1U:第二個倖存區的使用大小

EC:伊甸園區的大小

EU:伊甸園區的使用大小

OC:老年代大小

OU:老年代使用大小

MC:方法區大小

MU:方法區使用大小

CCSC:壓縮類空間大小

CCSU:壓縮類空間使用大小

YGC:年輕代垃圾回收次數

YGCT:年輕代垃圾回收消耗時間

FGC:老年代垃圾回收次數

FGCT:老年代垃圾回收消耗時間

GCT:垃圾回收消耗總時間

jmap

打印出jvm中全部對象的分佈情況

jmap -histo 6901 | head -20

 

分析:ScheduledThreadPoolExecutor$ScheduledFutureTask,UserInfo等對象都高達159956個。主要是類FullGCProblem裏的線程池引起的。

小結

線程池中的任務數永遠多於線程數,初始50個核心線程數會一直被佔滿,那麼任務會一直進入阻塞隊列,阻塞隊列任務會不斷地增加,每個隊列元素都引用一個對象。導致內存不斷的增加,而executor又是一個GCroots,所以堆中的對象空間無法回收。導致jvm內存耗盡,GC在不停地併發回收,CPU資源被佔滿。

內存泄露導致回收率低

內存泄露,引用內存回收率低,而導出內存的可用空間減少,導致內存快速被佔滿垃圾回收器不斷FULLGC。最終CPU100%。

代碼

//模擬棧的容器
public class Stack {

public Object[] elements;//
數組來保存

public int getSize() {
return size;
}

private int size =0;
private static final int Cap = 300000;

public Stack() {
elements = new Object[Cap];
}

public void push(Object e){ //
入棧
elements[size] = e;
size++;
}
public Object pop(){ //
出棧
    size = size -1;
Object o = elements[size];
//elements[size] =null; //
不用---引用幹掉,GC可以正常回收次對象
return o;
}
}

public class UseStack {
static Stack stack = new Stack(); //new
一個棧

public static void main(String[] args) throws Exception {


for (int i = 0; i < 100000; i++) {//10
萬的數據入棧
stack.push(new String[1 * 1000]); //
入棧
}
for (int i = 0; i < 100000; i++) {//10
萬的數據出棧
Object o1 = stack.pop(); //
出棧
}

ArrayList al = new ArrayList();
for (int i = 0; i < 100000; i++) {
al.add(new String[1 * 1000]);
}

Thread.sleep(Integer.MAX_VALUE);

}


}

配置 -XX:+HeapDumpOnOutOfMemoryError -Xms500M -Xmx500M

啓動程序: java -cp demofullgcuse.jar -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGC -Xms500M -Xmx500M com.hdk.demofullgc.UseStack 配置說明: -Xms500M -Xmx500M 配置剛好內存10萬數據大小 -XX:-HeapDumpOnOutOfMemoryError 默認關閉,建議開啓,在 java.lang.OutOfMemoryError 異常出現時,輸出一個 dump 文件,記錄當時的堆內存快照。

 

內存不夠產生了oom。生成dump 文件java_pid8839.hprof。

mat分析工具

MAT 工具是基於 Eclipse 平臺開發的,本身是一個 Java 程序,是一款很好的內存分析工具。

 

分析結果:同一個種類型實例,83%400M的內存。無法釋放。代碼定位Stack類代碼中。大量的對應無法釋放導致GC無法回收造成oom。解決方法:在Stack中的出棧方法pop中添加 elements[size] =null;告訴GC出棧的對象可以回收。問題即可解決。

 

總結:

在 JVM 出現性能問題的時候。(表現上是 CPU100%,內存一直佔用)

CPU佔用過高常見原因

 

CPU佔用高的解決策略

命令行工具

JPS

列出當前機器上正在運行的虛擬機進程,JPS 從操作系統的臨時目錄上去找(所以有一些信息可能顯示不全)

-q :僅僅顯示進程,

-m:輸出主函數傳入的參數. 下的 hello 就是在執行程序時從命令行輸入的參數

-l: 輸出應用程序主類完整 package 名稱或 jar 完整名稱. -v: 列出 jvm 參數, -Xms20m -Xmx50m

jstat

是用於監視虛擬機各種運行狀態信息的命令行工具。它可以顯示本地或者遠程虛擬機進程中的類裝載、內存、垃圾收集、JIT 編譯等運行數據,在沒有 GUI圖形界面,只提供了純文本控制檯環境的服務器上,它將是運行期定位虛擬機性能問題的首選工具。 常用參數: -class (類加載器)

-compiler (JIT)

-gc (GC 堆狀態)

-gccapacity (各區大小)

-gccause (最近一次 GC 統計和原因)

-gcnew (新區統計)

-gcnewcapacity (新區大小)

-gcold (老區統計)

-gcoldcapacity (老區大小)

-gcpermcapacity (永久區大小)

-gcutil (GC 統計彙總)

-printcompilation (HotSpot 編譯統計)

比如說:我們要統計 GC,就是垃圾回收,那麼只需要使用這樣的命令。 jstat-gc 13616 (這個 13616 是 JVM 的進程,通過 JPS 命令得到),這樣統計出來是的實時值。 所以很多情況下,我們爲了看變化值的,可以這麼玩。

jinfo

查看和修改虛擬機的參數 jinfo –sysprops 可以查看由 System.getProperties()取得的參數 jinfo –flag 未被顯式指定的參數的系統默認值 jinfo –flags(注意 s)顯示虛擬機的參數

VM 參數分類

JVM 的命令行參數參考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

jmap

用於生成堆轉儲快照(一般稱爲 heapdump 或 dump 文件)。jmap 的作用並不僅僅是爲了獲取 dump 文件,它還可以查詢 finalize 執行隊列、Java 堆和永 久代的詳細信息,如空間使用率、當前用的是哪種收集器等。和 jinfo 命令一樣,jmap 有不少功能在 Windows 平臺下都是受限的,除了生成 dump 文件的 -dump 選項和用於查看每個類的實例、空間佔用統計的-histo 選項在所有操作系統都提供之外,其餘選項都只能在 Linux/Solaris 下使用。

-heap 打印 heap 的概要信息

jmap –heap

Heap Configuration: ##堆配置情況,也就是 JVM 參數配置的結果[平常說的 tomcat 配置 JVM 參數,就是在配置這些]

MinHeapFreeRatio = 40 ##最小堆使用比例

MaxHeapFreeRatio = 70 ##最大堆可用比例

MaxHeapSize = 2147483648 (2048.0MB) ##最大堆空間大小

NewSize = 268435456 (256.0MB) ##新生代分配大小

MaxNewSize = 268435456 (256.0MB) ##最大可新生代分配大小

OldSize = 5439488 (5.1875MB) ##老年代大小

NewRatio = 2 ##新生代比例

SurvivorRatio = 8 ##新生代與 suvivor 的比例

PermSize = 134217728 (128.0MB) ##perm 區 永久代大小

MaxPermSize = 134217728 (128.0MB) ##最大可分配 perm 區 也就是永久代大小

Heap Usage: ##堆使用情況【堆內存實際的使用情況】

New Generation (Eden + 1 Survivor Space): ##新生代(伊甸區 Eden 區 + 倖存區 survior(1+2)空間)

capacity = 241631232 (230.4375MB) ##伊甸區容量

used = 77776272 (74.17323303222656MB) ##已經使用大小

free = 163854960 (156.26426696777344MB) ##剩餘容量

32.188004570534986% used ##使用比例

Eden Space: ##伊甸區

capacity = 214827008 (204.875MB) ##伊甸區容量

used = 74442288 (70.99369812011719MB) ##伊甸區使用

free = 140384720 (133.8813018798828MB) ##伊甸區當前剩餘容量

34.65220164496263% used ##伊甸區使用情況

From Space: ##survior1 區

capacity = 26804224 (25.5625MB) ##survior1 區容量

used = 3333984 (3.179534912109375MB) ##surviror1 區已使用情

free = 23470240 (22.382965087890625MB) ##surviror1 區剩餘容量

12.43827838477995% used ##survior1 區使用比例

To Space: ##survior2 區

capacity = 26804224 (25.5625MB) ##survior2 區容量

used = 0 (0.0MB) ##survior2 區已使用情況

free = 26804224 (25.5625MB) ##survior2 區剩餘容量

0.0% used ## survior2 區使用比例

PS Old Generation: ##老年代使用情況

capacity = 1879048192 (1792.0MB) ##老年代容量

used = 30847928 (29.41887664794922MB) ##老年代已使用容量

free = 1848200264 (1762.5811233520508MB) ##老年代剩餘容量

1.6416783843721663% used ##老年代使用比例

 

-histo 打印每個 class 的實例數目,內存佔用,類全名信息.

jmap –histo jmap –histo:live 如果 live 子參數加上後,只統計活的對象數量

 

但是這樣顯示太多了,一般在 linux 上會這麼操作

jmap –histo 1196 | head -20 (這樣只會顯示排名前 20 的數據 )

-dump 生成的堆轉儲快照(比較重要)

jmap -dump:live,format=b,file=heap.bin

Sun JDK 提供 jhat(JVM Heap Analysis Tool)命令與 jmap 搭配使用,來分析 jmap

jhat

jhat dump 文件名 後屏幕顯示"Server is ready."的提示後,用戶在瀏覽器中鍵入 http://localhost:7000/就可以訪問詳情

 

使用 jhat 可以在服務器上生成堆轉儲文件分析(一般不推薦,畢竟佔用服務器的資源,,比如一個文件就有 1 個 G 的話就需要大約喫一個 1G 的內存資源)

jstack

(Stack Trace for Java)命令用於生成虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等都是導致線程長時間停頓的常見原因。

在代碼中可以用 java.lang.Thread 類的 getAllStackTraces()方法用於獲取虛擬機中所有線程的StackTraceElement 對象。使用這個方法可以通過簡單的幾行代碼就完成 jstack 的大部分功能,在實際項目中不妨調用這個方法做個管理員頁面,可以隨時使用瀏覽器來查看線程堆棧。)一般來說 jstack 主要是用來排查是否有死鎖的情況。

Arthas

官方文檔參考 https://alibaba.github.io/arthas/ Arthas 是 Alibaba 開源的 Java 診斷工具,深受開發者喜愛。Arthas 支持 JDK 6+,支持 Linux/Mac/Windows,採用命令行交互模式,同時提供豐富的 Tab 自動補全功能,進一步方便進行問題的定位和診斷

可視化工具

Jconsole,visualvm 這兩款使用比較簡單,一般java應用都是運行在linux平臺。所以這裏忽略了。

命令工具總結

生產服務器推薦開啓 -XX:-HeapDumpOnOutOfMemoryError 默認關閉,建議開啓,在 java.lang.OutOfMemoryError 異常出現時,輸出一個 dump 文件,記錄當時的堆內存快照。 -XX:HeapDumpPath=./java_pid.hprof 用來設置堆內存快照的存儲文件路徑,默認是 java

調優之前開啓、調優之後關閉 -XX:+PrintGC 調試跟蹤之打印簡單的 GC 信息參數:

-XX:+PrintGCDetails,

+XX:+PrintGCTimeStamps 打印詳細的 GC 信息 -Xlogger:logpath 設置 gc 的日誌路,如: -Xlogger:log/gc.log, 將 gc.log 的路徑設置到當前目錄的 log 目錄下. 應用場景:將 gc 的日誌獨立寫入日誌文件,將 GC 日誌與系統業務日誌進行了分離,方便開發人員進行追蹤分析

** 考慮使用** -XX:+PrintHeapAtGC, 打印推信息 參數設置: -XX:+PrintHeapAtGC應用場景: 獲取 Heap 在每次垃圾回收前後的使用狀況

-XX:+TraceClassLoading參數方法: -XX:+TraceClassLoading

應用場景:在系統控制檯信息中看到 class 加載的過程和具體的 class 信息,可用以分析類的加載順序以及是否可進行精簡操作。

-XX:+DisableExplicitGC 禁止在運行期顯式地調用 System.gc()

調優經驗分享

GC 頻率 高頻的 FullGC 會給系統帶來非常大的性能消耗,雖然 MinorGC 相對 FullGC 來說好了許多,但過多的 MinorGC 仍會給系統帶來壓力。對應:調整堆內存空間減少 GC,分析堆內存基本被用完,而且存在大量 MinorGC 和 FullGC,這意味着我們的堆內存嚴重不足,這個時候我們需要調大堆內存空間。

添加配置 -Xms1500m -Xmx1500m 增加堆內存空間 內存比例 內存指的是堆內存大小,堆內存又分爲年輕代內存和老年代內存。堆內存不足,會增加 MinorGC ,影響系統性能。

MinorGC比較頻發可以通過-Xmn 增加年輕代大小,降低 Minor GC 的頻率 。-XX:SurvivorRatio調整大survivor區來減少觸發動態年齡判斷。

-Xmn1000m -XX:SurvivorRatio=7 修改合適的大小。

-XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128 M 設置一個夠用值

元空間一般啓動後就不會有太多的變化,所以把MetaspaceSize和MaxMetaspaceSize設置成一樣。我們可以設定爲 128M,節約內存

吞吐量 頻繁的 GC 將會引起線程的上下文切換,增加系統的性能開銷,從而影響每次處理的線程請求,最終導致系統的吞吐量下降。

-XX:ParallelGCThreads=8 線程數可以根據你的服務器資源情況來設定(要速度快的話可以設置大點,根據 CPU 的情況來定,一般設置成 CPU 的整 數倍

延時 JVM 的 GC 持續時間也會影響到每次請求的響應時間。

-XX:MaxTenuringThreshold=2 這個是分代年齡(年齡爲 2 就可以進入老年代),因爲我們基本上都使用的是 Spring 架構,Spring 中很多的 bean 是 長期要存活的,沒有必要在 Survivor 區過渡太久,MaxTenuringThreshold默認是15,所以可以設定爲 2,讓大部分的 Spring 的內部的一些對象進入老年代。

-XX:+UseConcMarkSweepGC 如果是業務響應時間優先的,所以還是可以使用 CMS 垃圾回收器或者 G1 垃圾回收器。

推薦策略

  1. 新生代大小選擇
  1. 老年代大小選擇

GC 性能衡量指標

分析 GC 日誌

通過 JVM 參數預先設置 GC 日誌,幾種 JVM 參數設置如下:

-XX:+PrintGC 輸出 GC 日誌

-XX:+PrintGCDetails 輸出 GC 的詳細日誌

-XX:+PrintGCTimeStamps 輸出 GC 的時間戳(以基準時間的形式)

-XX:+PrintGCDateStamps 輸出 GC 的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)

-XX:+PrintHeapAtGC 在進行 GC 的前後打印出堆的信息

-Xloggc:../logs/gc.log 日誌文件的輸出路徑

 

命令格式

java -jar -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs jvm-1.0-SNAPSHOT.jar

日誌查看工具gcViewer,Gceasy https://gceasy.io/

 

GC 調優策略

  • 降低 Minor GC 頻率
  • 由於新生代空間較小,Eden 區很快被填滿,就會導致頻繁 Minor GC,因此我們可以通過增大新生代空間來降低 Minor GC 的頻率。 單次 Minor GC 時間是由兩部分組成:T1(掃描新生代)和 T2(複製存活對象)。
  • 降低 Full GC 的頻率
  • 由於堆內存空間不足或老年代對象太多,會觸發 Full GC,頻繁的 Full GC 會帶來上下文切換,增加系統的性能開銷 。
  • 減少創建大對象:在平常的業務場景中,我們一次性從數據庫中查詢出一個大對象用於 web 端顯示。比如,一次性查詢出 60 個字段的業務操作,這種大對象如果超過年輕代最大對象閾值,會被直接創建在老年代;即使被創建在了年輕代,由於年輕代的內存空間有限,通過 Minor GC 之後也會進入到老 年代。這種大對象很容易產生較多的 Full GC。
  • 增大堆內存空間:在堆內存不足的情況下,增大堆內存空間,且設置初始化堆內存爲最大堆內存,也可以降低 Full GC 的頻率。
  • 選擇合適的 GC 回收器: 如果要求每次操作的響應時間必須在 500ms 以內。這個時候我們一般會選擇響應速度較快的 GC 回收器,堆內存比較小的情況下(<6G)選擇 CMS(Concurrent Mark Sweep)回收器和堆內存比較大的情況下(>8G)G1 回收器。
  • GC調優小結 GC 調優是個很複雜、很細緻的過程,要根據實際情況調整,不同的機器、不同的應用、不同的性能要求調優的手段都是不同的,一般調優的思路都是"測試 - 分析 - 調優",任何調優都需要結合場景,明確已知問題和性能目標,不能爲了調優而調優,以免引入新的 Bug,帶來風險和弊端。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章