沒有監控過JVM內存的職場生涯,是不完美的
本文轉載自微信公眾號「程序新視界」,作者丑胖俠二師兄。轉載本文請聯系程序新視界公眾號。
前言
如果你從事Java開發多年,還沒用過JVM分析工具,還沒嘗試著通過對JVM的dump日志來進行故障定位和性能調優,那么可以說是你職場生涯中的一大遺憾,也是一種能力的缺失。
這篇文章就基于一款JDK自帶的工具(VisualVM),然后編寫內存溢出的案例,帶大家體驗一下JVM分析的入門。文中涉及到多個知識點的融合與實戰經驗分享,讀者可留意一下。
VisualVM簡介
VisualVM是Netbeans的profile子項目,在JDK6.0 update 7 中自帶,能夠監控線程,內存情況,查看方法的CPU時間和內存中的對象,已被GC的對象,反向查看分配的堆棧(如100個String對象分別由哪幾個對象分配出來的)。
如果已經正確配置classpath路徑,VisualVM的啟動非常簡單,只需在命令行輸入jvisualvm即可啟動圖形化界面。VisualVM不僅支持本機監控,還支持遠程監控。
遠程監控配置稍微復雜一些,這里以本地監控為示例進行演示。至于生產環境,可選擇遠程監控,也可配合jmap先生成dump文件,然后下載dump文件進行分析。
VisualVM功能界面
啟動VisualVM之后,先來看一下有助于JVM分析的幾項功能。這里先以本地啟動的Idea為例來進行展示。
概述
進入VisualVM之后,點擊左邊的對應進程,首先展示的是【概述】內容:
概述中顯示了JVM、Java版本、dump批次等信息,在實戰中這里的信息可用來進行信息核對。特別是JVM參數和系統屬性項的核對。
曾遇到一個場景,就是通過啟動Java程序時JVM參數的位置寫錯了,導致JVM參數并不生效。
比如如下指令,由于JVM的參數寫在了最后,會導致參數設置無效。
- java -jar app.jar -Xms256m -Xmx512m
而正確的寫法應該是如下:
- java -Xms256m -Xmx512m -jar app.jar
上面這種情況,通過該工具可以輕易的看出JVM參數項里面并沒有指定的參數值。
監視
監視界面是用的比較多的一個界面,通過該界面可以查看CPU使用情況、堆和Metaspace的使用情況、線程的使用情況、類的加載情況等。
通過對堆和Metaspace的使用情況分析,可以看到對應內存空間的使用和增長情況,可進行合理調整和規劃。
點擊右上角的“堆 dump”,會基于點擊的時間節點生成dump文件。
概要部分會顯示生成dump文件的時間節點和存儲路徑。我們用來分析內存主要是在此頁面中的“類”菜單內。
進入可查看在堆中不同實例占用的內存大小。雙擊類名,即可進入查看“實例數”,也就是具體類的實例詳情。
而我們在內存分析時最重要的其實就是“類”的數量。在了解了上述的基本操作之后,我們就用一個實例來模擬分析一下內存溢出的場景吧。
內存溢出場景構建
先寫一段代碼,用來模擬內存溢出,也就是創建一個Map,然后向其中不斷的新增對象。同時在程序處理的過程中讓線程睡眠或死循環來方便通過VisualVM進行查看。
測試代碼如下:
- public class MemoryLeakTest {
- /**
- * 聲明緩存對象
- */
- private static final Map<String, TestMemory> CACHE_MAP = new HashMap<>();
- public static void main(String[] args) {
- try {
- //給打開visualVm時間
- Thread.sleep(10000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- //循環添加對象到緩存
- for (int i = 0; i < 1000000; i++) {
- TestMemory t = new TestMemory();
- CACHE_MAP.put("key" + i, t);
- }
- System.out.println("-------1------");
- //為dump出堆提供時間
- try {
- //給打開visualVm時間
- Thread.sleep(10000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- for(int i=0; i<1000000;i++){
- TestMemory t = new TestMemory();
- CACHE_MAP.put("key"+i,t);
- }
- }
- }
TestMemory類代表生成中的業務類。
- public class TestMemory {
- }
為了讓程序更快的達到內存的閾值,我們啟動時可限制JVM的大小,這里設置為:
- -Xms128m -Xmx128m
另外,為了分析堆的內存結構中每個區域(新生代、老年代)的內存使用情況,可在VisualVM的“工具”、“插件”中安裝Visual GC插件。該插件的使用后續會看到效果。
一切準備妥當,下面來進行驗證。
內存溢出分析
下面我們就來啟動程序,啟動VisualVM來進行內存溢出的分析。
當程序執行的過程中,我們會發現“堆”內存會出現一個快速增加的曲線。
這個過程中Metaspace也在隨之增長。
打開Visual GC界面,我們會看到面試中被問過很多遍的堆內存結構:
通過這張圖,可以直觀的看到堆內存中的老年代、新生代、Metaspace空間(JDK8),還有新生代中的Eden、S0(Survivor From)、S1(Survivor To),而且它們的分配比例也有一個比較直觀的展示。通過這種形式,是不是可以更直觀的學習堆內存結構呢?
這里Eden已經被填滿、S0和S1為空,老年代也幾乎被填滿(因為垃圾收集無法清除持有引用的對象)。
最重要的是你會發現針對老年代在20分鐘內進行了3850次垃圾回收。也就是說已經觸發了頻繁的Full GC操作,而且內存并沒有被釋放掉。在生產系統中,當你看到系統在頻繁的進行Full GC操作,那是JVM在釋放一個很恐怖的信號了。
上面說了一些表象的內容,現在真正開進行內存分析了。回到上面提到的“監視”、“堆dump”、“類”中,可以看到下圖:
可以看到,堆中存在著100萬個TestMemory對象。當你看到堆中有類似的大量的對象存在,你應該意識到此處可能有內存泄露。也就是大量的對象被創建,而沒有被“順利”回收。我們這里沒被回收的原因是對象被放在了靜態變量里面了。
上面已經提到,你還可以進一步雙擊對象名稱,去查看對象的詳細信息。
通過上面步驟,基本上可以定位到哪些對象的處理出現了問題,此時再回到代碼中針對相應的代碼進行排查,便可快速定位內存溢出的問題所在。其中我們需要特別留意上述過程中VisualVM為我們提供的那些報警信號和數據呈現。
小結
本文我們是在講VisualVM的使用,也是在講線上JVM的排查,也是在講JVM的內存結構,還是在講如何去構造一個內存溢出(bug)的場景。但講什么并不重要,關鍵是看,通過這篇文章,你重溫了什么,學到了什么,又收獲了什么。