JVM性能監測及調優(5)

內存持續上升,我該如何排查問題?

我想你肯定遇到過內存溢出,或是內存使用率過高的問題。碰到內存持續上升的情況,其實我們很難從業務日誌中查看到具體的問題,那麼面對多個進程以及大量業務線程,我們該如何精準地找到背後的原因呢?

常用的監控和診斷內存工具

工欲善其事,必先利其器。平時排查內存性能瓶頸時,我們往往需要用到一些 Linux 命令行或者 JDK 工具來輔助我們監測系統或者虛擬機內存的使用情況,下面我就來介紹幾種好用且常用的工具。

Linux 命令行工具之 top 命令

top 命令是我們在 Linux 下最常用的命令之一,它可以實時顯示正在執行進程的 CPU 使用率、內存使用率以及系統負載等信息。其中上半部分顯示的是系統的統計信息,下半部分顯示的是進程的使用率統計信息。

除了簡單的 top 之外,我們還可以通過 top -Hp pid 查看具體線程使用系統資源情況:

Linux 命令行工具之 vmstat 命令

vmstat 是一款指定採樣週期和次數的功能性監測工具,我們可以看到,它不僅可以統計內存的使用情況,還可以觀測到 CPU 的使用率、swap 的使用情況。但 vmstat 一般很少用來查看內存的使用情況,而是經常被用來觀察進程的上下文切換。

r:等待運行的進程數;
b:處於非中斷睡眠狀態的進程數;
swpd:虛擬內存使用情況;
free:空閒的內存;
buff:用來作爲緩衝的內存數;
si:從磁盤交換到內存的交換頁數量;
so:從內存交換到磁盤的交換頁數量;
bi:發送到塊設備的塊數;
bo:從塊設備接收到的塊數;
in:每秒中斷數;
cs:每秒上下文切換次數;
us:用戶 CPU 使用時間;
sy:內核 CPU 系統使用時間;
id:空閒時間;
wa:等待 I/O 時間;
st:運行虛擬機竊取的時間。

Linux 命令行工具之 pidstat 命令

pidstat 是 Sysstat 中的一個組件,也是一款功能強大的性能監測工具,我們可以通過命令:yum install sysstat 安裝該監控組件。之前的 top 和 vmstat 兩個命令都是監測進程的內存、CPU 以及 I/O 使用情況,而 pidstat 命令則是深入到線程級別。

通過 pidstat -help 命令,我們可以查看到有以下幾個常用的參數來監測線程的性能:

常用參數:
-u:默認的參數,顯示各個進程的 cpu 使用情況;
-r:顯示各個進程的內存使用情況;
-d:顯示各個進程的 I/O 使用情況;
-w:顯示每個進程的上下文切換情況;
-p:指定進程號;
-t:顯示進程中線程的統計信息。

我們可以通過相關命令(例如 ps 或 jps)查詢到相關進程 ID,再運行以下命令來監測該進程的內存使用情況:

其中 pidstat 的參數 -p 用於指定進程 ID,-r 表示監控內存的使用情況,1 表示每秒的意思,3 則表示採樣次數。

其中顯示的幾個關鍵指標的含義是:

Minflt/s:任務每秒發生的次要錯誤,不需要從磁盤中加載頁;
Majflt/s:任務每秒發生的主要錯誤,需要從磁盤中加載頁;
VSZ:虛擬地址大小,虛擬內存使用 KB;
RSS:常駐集合大小,非交換區內存使用 KB。

如果我們需要繼續查看該進程下的線程內存使用率,則在後面添加 -t 指令即可:

我們知道,Java 是基於 JVM 上運行的,大部分內存都是在 JVM 的用戶內存中創建的,所以除了通過以上 Linux 命令來監控整個服務器內存的使用情況之外,我們更需要知道 JVM 中的內存使用情況。JDK 中就自帶了很多命令工具可以監測到 JVM 的內存分配以及使用情況。

JDK 工具之 jstat 命令

jstat 可以監測 Java 應用程序的實時運行情況,包括堆內存信息以及垃圾回收信息。我們可以運行 jstat -help 查看一些關鍵參數信息:

再通過 jstat -option 查看 jstat 有哪些操作:

-class:顯示 ClassLoad 的相關信息;
-compiler:顯示 JIT 編譯的相關信息;
-gc:顯示和 gc 相關的堆信息;
-gccapacity:顯示各個代的容量以及使用情況;
-gcmetacapacity:顯示 Metaspace 的大小;
-gcnew:顯示新生代信息;
-gcnewcapacity:顯示新生代大小和使用情況;
-gcold:顯示老年代和永久代的信息;
-gcoldcapacity :顯示老年代的大小;
-gcutil:顯示垃圾收集信息;
-gccause:顯示垃圾回收的相關信息(通 -gcutil),同時顯示最後一次或當前正在發生的垃圾回收的誘因;
-printcompilation:輸出 JIT 編譯的方法信息。

它的功能比較多,在這裏我例舉一個常用功能,如何使用 jstat 查看堆內存的使用情況。我們可以用 jstat -gc pid 查看:

S0C:年輕代中 To Survivor 的容量(單位 KB);
S1C:年輕代中 From Survivor 的容量(單位 KB);
S0U:年輕代中 To Survivor 目前已使用空間(單位 KB);
S1U:年輕代中 From Survivor 目前已使用空間(單位 KB);
EC:年輕代中 Eden 的容量(單位 KB);
EU:年輕代中 Eden 目前已使用空間(單位 KB);
OC:Old 代的容量(單位 KB);
OU:Old 代目前已使用空間(單位 KB);
MC:Metaspace 的容量(單位 KB);
MU:Metaspace 目前已使用空間(單位 KB);
YGC:從應用程序啓動到採樣時年輕代中 gc 次數;
YGCT:從應用程序啓動到採樣時年輕代中 gc 所用時間 (s);
FGC:從應用程序啓動到採樣時 old 代(全 gc)gc 次數;
FGCT:從應用程序啓動到採樣時 old 代(全 gc)gc 所用時間 (s);
GCT:從應用程序啓動到採樣時 gc 用的總時間 (s)。

JDK 工具之 jstack 命令

它是一種線程堆棧分析工具,最常用的功能就是使用 jstack pid 命令查看線程的堆棧信息,通常會結合 top -Hp pid 或 pidstat -p pid -t 一起查看具體線程的狀態,也經常用來排查一些死鎖的異常。

每個線程堆棧的信息中,都可以查看到線程 ID、線程的狀態(wait、sleep、running 等狀態)以及是否持有鎖等。

JDK 工具之 jmap 命令
jmap可以查看堆內存初始化配置信息以及堆內存的使用情況。那麼除了這個功能,我們其實還可以使用 jmap 輸出堆內存中的對象信息,包括產生了哪些對象,對象數量多少等

我們可以用 jmap 來查看堆內存初始化配置信息以及堆內存的使用情況:

我們可以使用 jmap -histo[:live] pid 查看堆內存中的對象數目、大小統計直方圖,如果帶上 live 則只統計活對象:

我們可以通過 jmap 命令把堆內存的使用情況 dump 到文件中:

我們可以將文件下載下來,使用 MAT 工具打開文件進行分析:

下面我們用一個實戰案例來綜合使用下剛剛介紹的幾種工具,具體操作一下如何分析一個內存泄漏問題。

實戰演練

我們平時遇到的內存溢出問題一般分爲兩種,一種是由於大峯值下沒有限流,瞬間創建大量對象而導致的內存溢出;另一種則是由於內存泄漏而導致的內存溢出。

使用限流,我們一般就可以解決第一種內存溢出問題,但其實很多時候,內存溢出往往是內存泄漏導致的,這種問題就是程序的 BUG,我們需要及時找到問題代碼。

下面我模擬了一個內存泄漏導致的內存溢出案例,我們來實踐一下。

我們知道,ThreadLocal 的作用是提供線程的私有變量,這種變量可以在一個線程的整個生命週期中傳遞,可以減少一個線程在多個函數或類中創建公共變量來傳遞信息,避免了複雜度。但在使用時,如果 ThreadLocal 使用不恰當,就可能導致內存泄漏。

這個案例的場景就是 ThreadLocal,下面我們模擬對每個線程設置一個本地變量。運行以下代碼,系統一會兒就發送了內存溢出異常:

@RequestMapping(value = "/test0")
public String test0(HttpServletRequest request) {
    ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();
    localVariable.set(new Byte[4096*1024]);// 爲線程添加變量
    return "success";
}

在啓動應用程序之前,我們可以通過 HeapDumpOnOutOfMemoryError 和 HeapDumpPath 這兩個參數開啓堆內存異常日誌,通過以下命令啓動應用程序:

java -jar -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log heapTest-0.0.1-SNAPSHOT.jar

首先,請求 test0 鏈接 10000 次,這個時候我們請求 test0 的接口報異常了。

通過日誌,我們很好分辨這是一個內存溢出異常。我們首先通過 Linux 系統命令查看進程在整個系統中內存的使用率是多少,最簡單就是 top 命令了。

從 top 命令查看進程的內存使用情況,可以發現在機器只有 8G 內存且只分配了 4G 內存給 Java 進程的情況下,Java 進程內存使用率已經達到了 55%,再通過 top -Hp pid 查看具體線程佔用系統資源情況。

再通過 jstack pid 查看具體線程的堆棧信息,可以發現該線程一直處於 TIMED_WAITING 狀態,此時 CPU 使用率和負載並沒有出現異常,我們可以排除死鎖或 I/O 阻塞的異常問題了。

我們再通過 jmap 查看堆內存的使用情況,可以發現,老年代的使用率幾乎快佔滿了,而且內存一直得不到釋放:

通過以上堆內存的情況,我們基本可以判斷系統發生了內存泄漏。下面我們就需要找到具體是什麼對象一直無法回收,什麼原因導致了內存泄漏。

我們需要查看具體的堆內存對象,看看是哪個對象佔用了堆內存,可以通過 jmap 查看存活對象的數量:

Byte 對象佔用內存明顯異常,說明代碼中 Byte 對象存在內存泄漏,我們在啓動時,已經設置了 dump 文件,通過 MAT 打開 dump 的內存日誌文件,我們可以發現 MAT 已經提示了 byte 內存異常:

再點擊進入到 Histogram 頁面,可以查看到對象數量排序,我們可以看到 Byte[]數組排在了第一位,選中對象後右擊選擇 with incomming reference 功能,可以查看到具體哪個對象引用了這個對象。

在這裏我們就可以很明顯地查看到是 ThreadLocal 這塊的代碼出現了問題。

總結

在一些比較簡單的業務場景下,排查系統性能問題相對來說簡單,且容易找到具體原因。但在一些複雜的業務場景下,或是一些開源框架下的源碼問題,相對來說就很難排查了,有時候通過工具只能猜測到可能是某些地方出現了問題,而實際排查則要結合源碼做具體分析。

可以說沒有捷徑,排查線上的性能問題本身就不是一件很簡單的事情,除了將今天介紹的這些工具融會貫通,還需要我們不斷地去累積經驗,真正做到性能調優。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章