JVM - 工欲善其事必先利其器之虛擬機工具(下)
上一章我們介紹瞭如果使用JDK內置的一些命令,去分析、優化以及幫助我們解決應用程序中的一些問題。確實那些命令雖然使用起來十分簡單,但是我們也能感受到其功能的強大。不過由於其採用命令行的特性,在一定的程度上也提升了我們的閱讀和使用門檻。
這一章我們就來介紹兩款JVM的可視化工具,更直觀的可視化界面對我們分析程序可以說是事半功倍。
1.可視化虛擬機工具JConsole
1.1 JConsole是什麼?
JDK除了提供了大量命令行工具外,還內置了兩個強大的可視化工具:
JConsole
、VisualVM
。
JConsole (Java Monitoring and Management Console)
是一款基於JMX的可視化監控、管理工具,簡單來說就是我們可以通過它來監控和優化Java應用程序性能。下面我們就來看下它和我們之前使用的命令行工具差別到底在哪裏。
1.2 JConsole的使用
使用方式很簡單,我們先把之前
springboot-jvm
項目啓動好,這裏示例代碼地址在第一章頭部會貼出,大家也可以通過官網或者自己去構建一個簡單的程序跑起來就ok了。
我們打開JDK安裝目錄,在bin
目錄下有一個jconsole
應用程序,點擊即可啓動。或者大家配置好了JAVA_HOME
的話在cmd命令行直接輸入jconsole
也是一樣的效果。另外官網也提供了JConsole的使用方法【Using Jconsole - Java SE Monitoring and Management Guide】,大家有興趣可以去了解一哈。
這裏可以看到它會自動搜索我們本機的所有虛擬機進程,不需要和我們之前使用命令行一樣先自己去使用jps
找到進程對應pid,這裏我們直接選擇我們需要監控的進程即可。
1.2.1 概覽
連接成功後,我們可以看到整個界面十分簡潔明瞭。看得出來JConsole主要提供這幾方面的監控:內存、線程、類、VM概要、MBean。其中概述會把每個模塊大致情況反映出來,比如堆使用情況、線程使用情況、類加載情況以及CPU佔有情況。
另外這裏提一下,這裏每張圖標我們通過鼠標右鍵導出數據到CSV的,這裏我們有需要可以通過導出之後用其他方式對其進行特定分析。接下來我們來對每個模塊做一個簡單的瞭解。
1.2.2 內存
我們可以看到內存界面,相當於我們之前的
jstat
命令,主要用於監控虛擬機內存情況。
這些數據其實對於分析Java內存問題或者調優都是十分有價值的,我們可以查看堆內存、非堆內存、各分代的內存分配使用情況以及GC相關信息。
我們可以調整使用不同的GC以及JVM參數,通過觀察以得到最適合應用程序的參數組合,提升性能。
我們之前已經修煉了內存分配以及回收策略的功法,這裏我們藉着這個機會來親身感受一下內存分配和回收在JVM中真實的情況。
package com.ithzk.springbootjvm.memoryallocation;
import java.util.ArrayList;
/**
* @ Description : jconsole memory
* @ Author : zekunhu
* @ CreateDate : 2020/4/28 16:21
* @ UpdateUser : zekunhu
* @ UpdateDate : 2020/4/28 16:21
* @ UpdateRemark :
* @ Version : 1.0
*/
public class JconsoleMemoryAllocation {
public byte[] bytes = new byte[512 * 1024];
public static void main(String[] args) throws InterruptedException {
System.out.println("JconsoleMemoryAllocation main thread start...");
Thread.sleep(15000L);
allocation(6000);
}
private static void allocation(int size){
ArrayList<JconsoleMemoryAllocation> objects = new ArrayList<>();
for(int i = 0; i < size ; i++){
try {
Thread.sleep(100L);
objects.add(new JconsoleMemoryAllocation());
System.out.println("allocation current: " + i);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
這裏我們寫了一個很簡單的示例,主線程在睡眠一段時間後開始進行內存分配操作,每次構建一個對象都會進行至少512kb內存空間的分配,我們來觀察一下內存使用情況的變化。
首先這裏我們可以看到由於對象列表一直不會被收回,所以整個堆內存空間使用分配的增長會和時間成一個正比,這個不難理解。
我們再來看下Eden
區和Survivor
區,我們都知道當對象在Eden
區和Survivor From
區中進行分配時,如果分配達到某些條件或閾值後會放到Survivor To
區、Old Space
中,所以呈現一個折線的情況。
最後就是老年代了,這裏和整個堆空間內存分配情況差不多,主要是因爲新生代中的對象晉升到了老年代中。這裏我們通過可視化程序更貼切感受了一把JVM內存分配流程,想必大家對此也會印象深刻。
1.2.3 線程
線程界面這裏,相當於我們之前的
jstack
命令,主要用於監控線程情況。
這裏我們可以看到活躍線程數及其峯值,以及左下角可以看到每個線程的運行狀態,包括阻塞以及等待次數都有記錄。這裏最下面還有一個檢測死鎖
的按鈕,這裏我圖可能不太完善,後面的圖大家會清晰看到。這裏我們去編寫一個示例來模擬幾種線程常見的狀態。
package com.ithzk.springbootjvm.memoryallocation;
public class JconsoleDeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
private static Object waiting = new Object();
private static Object timedWaiting = new Object();
public static void main(String[] args) throws InterruptedException {
Thread.sleep(15000L);
new Thread(() -> {
synchronized (lock1){
try {
System.out.println(Thread.currentThread().getName() + "get lock1");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "get lock2");
}
}
}, "線程1").start();
new Thread(() -> {
synchronized (lock2){
try {
System.out.println(Thread.currentThread().getName() + "get lock2");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "get lock1");
}
}
}, "線程2").start();
new Thread(() -> {
synchronized (waiting){
try {
waiting.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"線程3").start();
new Thread(() -> {
synchronized (timedWaiting){
try {
timedWaiting.wait(60000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"線程4").start();
}
}
這個是我們之前模擬死鎖的示例,我們在裏面另外加了兩個線程用於模擬
WAITING
和TIMED_WAITING
狀態,我們通過JConsole
看看是不是和我們所預料的情況一樣。
我們啓動後通過JConsole
查看我們自定義的幾個線程,線程1和線程2都處於BLOCKED
狀態,而線程3處於WAITING
、線程4處於TIMED_WAITING
狀態,這和我們所預想的情況是一致的。這裏我們點擊檢測死鎖
或者線程旁邊死鎖頁籤。
這裏可以清楚地看到線程1和線程2都處於死鎖狀態,並且可以看到所需資源被哪個線程佔有。這裏我們正好回顧一下線程長時間停頓的幾種主要原因:等待外部資源(數據庫連接、網絡、設備等資源)、死循環、鎖等待。
有了JConsole
對線程的可視化界面分析,讓我們對線程運行的情況瞭解確實更方便、更高效。
1.2.4 類
這個界面主要就是用來監控類裝載和卸載的情況,使用的話還是比較少的,不細講。
1.2.5 VM概要
VM概要主要就是展示當前應用程序所使用的VM的一些相關配置,例如JVM版本、運行時間、使用何種垃圾收集器以及使用VM參數等等。大家有興趣可以自己看看,就是一個VM相關信息的概況。
1.2.6 MBean
這個界面主要就是展示MBean的一些相關信息。關於MBean我這邊並不是特別瞭解,查閱了一些資料大概意思就是在JMX中被管理的資源實例,通過MBean暴露的方法屬性,外界可以獲取被管理的資源狀態並且操作MBean。
在本質上,MBean就是一個Java Object,外界可以通過反射來獲取MBean的值或者調用其方法。MBean通過公共方法以及遵循特定的設計模式封裝了屬性和操作,以便暴露給管理應用程序。具體的話平時接觸很少,大家如果有需要可以自行去了解,關於MBean相關知識我就不誤人子弟了。
1.3 JConsole遠程連接
大家都知道真實的業務場景一般都部署在
Linux
一些服務器上,那麼我們如果要排查服務器上應用程序的問題要怎麼做呢?細心的小夥伴們都應該發現了,在我們最初連接JConsole時,提供了一個遠程連接的選項,沒錯就是它。
但是這裏我們連接也不是直接端上服務器IP就能連接上的,還需要我們配置一些JVM參數。
這裏我們要使用JConsole主要需要配置這幾個參數:-Dcom.sun.management.jmxremote.port=9999
、-Djava.rmi.server.hostname={ip}
、-Dcom.sun.management.jmxremote.ssl=false
、-Dcom.sun.management.jmxremote.authenticate=false
。
配置好之後我們啓動應用程序,使用遠程連接試一試。
可以看到這裏連接是成功的,這個時候我們就可以通過遠程連接去監控服務器上應用程序的一些內存、線程之類的具體情況,對應用程序進行問題排查和調優了。
2.可視化虛擬機工具VisualVM
除了
JConsole
,這裏給大家介紹另外一款JDK自帶的強大工具-VisualVM
。
2.1 VisualVM是什麼?
VisualVM
應該是目前爲止JDK內置工具中功能最強大的運行監控和故障處理工具。除了和JConsole
一樣支持可視化外它也能夠監控線程、內存情況、查看堆棧調用時間和消耗以及查看到內存中對象,還可以通過對象反向查看分配堆棧。
同樣它也支持本地和遠程連接應用程序,只要開啓連接就可以對正在運行應用程序達到實時監控的效果。
2.2 VisualVM的使用
VisualVM
使用也十分簡單,我們再次找到JDK安裝路徑下的bin目錄。
我們只要運行jvisualvm
就可以把VisualVM
啓動起來了。這裏我們可以簡單看下主界面,首先和JConsole
一樣也支持本地和遠程方式去連接應用程序,還是比較方便的;另外同樣無需我們自己再去手動執行jps
命令查找進程ID。
2.2.1 VisualVM插件
VisualVM
有一大亮點就是他可以安裝插件,這就使這款簡單的工具可以武裝得非常強大了。我們在主頁面點擊上方頁籤工具
-插件
。
這裏我們以Visual GC
爲例,選中後點擊安裝即可。
安裝成功後我們在已安裝界面激活插件即可使用。
這裏細心的小夥伴應該看到了又一個已下載
的頁籤。沒錯,除了官方自帶的插件庫,Visual VM
還支持我們自己去網上下載npm插件包進行激活,下載後從這裏添加插件即可使用。
2.2.2 VisualVM IDEA插件
我們在本地開發的時候每次需要都去JDK目錄下找還覺得繁瑣?完全沒問題,再讓你偷個懶好了。這裏以IDEA爲例,我們在
Plugins
中搜索VisualVM
下載這個插件重啓IDEA。
重新打開IDEA後發現在項目啓動按鈕旁邊多了兩個醒目的小按鈕,是不是很熟悉?這裏我們可以通過這兩個按鈕在Run
和Debug
模式下自動關聯啓動Visual VM
。
或者大家已經正常啓動了應用程序,此時有需要通過Visual VM
分析排查問題,這裏也提供了額外小按鈕調起。
另外如果第一次使用這個插件的話是需要配置相關文件路徑和目錄的。這樣使用起來是不是很方便呢?大家趕緊試試吧。
2.2.3 VisualVM 各模塊介紹
連上
Visual VM
主界面如上圖,首頁概述主要就是查看虛擬機進程相關信息、JVM參數、系統環境信息等,這是不是和我們之前所瞭解的jps
、jinfo
功能一樣。
接着就是監視
模塊,這裏和JConsole
一樣也提供了CPU、堆(包括Metaspace或方法區)、類、線程的大致使用情況,右上角還提供了一個堆Dump
功能,待會我們結合實例來看看怎樣巧妙地使用這個功能。
線程
界面的話主要也就是可視化每個線程運行狀態、消耗等相關信息,另外同樣也提供線程Dump
功能。
抽樣器
模塊可以對CPU
、內存
進行抽樣分析,還能夠對CPU樣例和堆內存分配生成快照。我們可以生成多份不同時間的快照文件,通過下圖按鈕進行比較多個文件的不同,從而可以分析出一些問題產生的原因。
這些就是Visual VM
給我們提供的強大功能模塊,大家應該記得我們之前還安裝過一個Visual GC
插件,這裏我們來看看它能帶給我們哪些信息。
Visual GC
將各代內存變化、GC頻率、GC時耗清楚地展現在我們面前。其實Visual VM
的很多功能JConsole
基本也有,但是我們會發現Visual VM
更加直觀並且數據分析更加全面,尤其是還提供添加插件功能,可以說是如虎添翼。
2.3 VisualVM實戰演練
實踐是檢驗整理的唯一標準,咱們直接來通過一些示例來看如何通過
Visual VM
分析程序問題。
2.3.1 OOM內存泄漏
說幹咱就幹,這裏我們先來模擬一個OOM的情況,這裏我們直接使用JConsole中
1.2.2 內存
的代碼即可。
public class JconsoleMemoryAllocation {
public byte[] bytes = new byte[512 * 1024];
public static void main(String[] args) throws InterruptedException {
System.out.println("JconsoleMemoryAllocation main thread start...");
Thread.sleep(15000L);
allocation(6000);
}
private static void allocation(int size){
ArrayList<JconsoleMemoryAllocation> objects = new ArrayList<>();
for(int i = 0; i < size ; i++){
try {
Thread.sleep(100L);
objects.add(new JconsoleMemoryAllocation());
System.out.println("allocation current: " + i);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
已經成功出現OOM了,我們通過
Visual VM
這裏我們通過
Visual GC
可以看到,當程序在持續運行過程中,老年代一直在進行GC操作,並且老年代中的堆內存使用量並未減少,大家通過我們對JVM的理解思考一下是什麼原因導致的?
沒錯,大概率是存在無法被回收的對象,所以才導致我們最後OOM了。這裏我們就需要進一步去分析是哪裏哪個對象泄漏了。
首先我們通過抽樣器中每個線程分配
,很清楚就可以看到可能是哪個線程發生了內存泄漏。這裏我們進一步分析具體對象,主要有兩種方式。
第一種:通過抽樣器中內存快照,我們在生成兩個不同時段的內存快照,使用我們開始提到的快照比照功能。
第二種就是通過監視
模塊的堆Dump,再對兩次結果進行比較。
這兩種方法都可以讓我們很快就確定具體泄漏的是哪種對象。
這裏我們可以看出兩次dump間隔時間內,JconsoleMemoryAllocation
和byte[]
實例在不停增加,說明這兩個對象引用的方法可能存在內存泄漏。
我們進一步選中JconsoleMemoryAllocation
,右鍵選擇在實例視圖中顯示
。
這裏左側實例數就是JconsoleMemoryAllocation
被創建的實例總數,右邊上半部分是選中JconsoleMemoryAllocation
實例的結構,這裏面可以看到它包含了一個很大的byte[]
數組。
而導致這些JconsoleMemoryAllocation
實例沒有得到回收的原因正是因爲右邊下半部分,這裏表明了這些實例被一個ArrayList
引用了,是不是通過Visual VM
一下就定位到了泄漏的根本原因?感覺肯定十分過癮,那我們再來一個。
2.3.2 線程死鎖
上面大家已經感受到了這款工具的強大,這裏我們再來模擬一個線程死鎖的情況,通過工具來分析究竟是哪裏。
public class JVisualVMDeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1){
try {
System.out.println(Thread.currentThread().getName() + "get lock1");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "get lock2");
}
}
}, "線程1").start();
new Thread(() -> {
synchronized (lock2){
try {
System.out.println(Thread.currentThread().getName() + "get lock2");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "get lock1");
}
}
}, "線程2").start();
}
}
這裏點開
線程
模塊,檢測到死鎖的字眼簡直不要太亮眼。
我們再去點擊旁邊的線程Dump
,這裏對哪個線程死鎖,等待哪個資源也是十分清晰的。
另外我們還可以通過採樣器
中CPU樣例裏的快照,很方便就能查看到出現問題的線程方法調用棧,從而快速排查問題。
2.4 VisualVM遠程連接
之前在
JConsole
遠程連接時,我們已經給服務器上的應用程序添加了jmx參數,這裏我們直接使用。
右鍵Visual VM
中遠程,添加遠程主機。
添加好遠程主機後,選擇添加的遠程主機右鍵添加JMX連接
,把服務器相關信息填上。
這樣就成功連接上遠程應用了,是不是十分簡單呢?
3.總結
這一章主要是接着上一章的JDK自帶命令行工具的基礎上,去介紹JDK自帶工具裏兩款強大的可視化虛擬機工具。
我們通過這兩章對JVM工具的介紹,如果能夠巧妙靈活地去使用這些工具,無論是對於我們排查問題還是優化程序都會帶來很大的便利。
我們在感嘆時代進步、工具變得更強大的同時,也要不斷地提升自我才能夠更快更好地融入這個世界。後面我會一些常見的應用場景介紹一些JVM優化相關的東西,希望和大家一起進步,加油。