應用JConsole學習Java GC
關于Java GC的知識,好多地方都講了很多,今天我用JConsole來學習一下Java GC的原理。
GC原理
在我的上一篇中介紹了Java運行時數據區,在了解這些的基礎上,對Java GC的理解能更清晰一些。
簡單來講,Java的內存分為堆和棧,其中堆是程序員用的內存,棧是系統用的內存。(這句話不一定正確,但可以這么理解)Java的內存管理主要是管理對象的分配和釋放,或者說內存的分配和回收。在C或C++語言里面,內存是要自己控制的,new之后要delete掉,否則很容易出現內存泄漏。(還記得當時寫C的痛苦,不過通過寫C代碼,很好的了解了內存的分配機制)在Java里面,分配內存和回收內存的事情是Jvm來管的。
Jvm有自己的機制來管理內存,具體的細節算法這里不講,主要講大概的處理方式。Jvm的GC主要處理堆內存,JVM內存模型中的堆可以細分為 Young Generation和Old Generation(又稱為Tenure Space),其中Young Generation又分為Eden Space(Eden是伊甸園的意思,老鷹樂隊有首歌叫Long Road out of Eden)和Survivor spaces。如下圖所示:
具體GC的過程如下:
- 在Young Generation中,有一個叫Eden Space的空間,主要是用來存放新生的對象,還有兩個Survivor Spaces(from、to),它們的大小總是一樣,它們用來存放每次垃圾回收后存活下來的對象。
- 在Old Generation中,主要存放應用程序中生命周期長的內存對象。
- 在Young Generation塊中,垃圾回收一般用Copying的算法,速度快。每次GC的時候,存活下來的對象首先由Eden拷貝到某個 SurvivorSpace,當Survivor Space空間滿了后,剩下的live對象就被直接拷貝到OldGeneration中去。因此,每次GC后,Eden內存塊會被清空。
- 在Old Generation塊中,垃圾回收一般用mark-compact的算法,速度慢些,但減少內存要求。
- 垃圾回收分多級,0級為全部(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上為部分垃圾回收,只會回收Young中的垃圾,內存溢出通常發生于OLD段或Perm段垃圾回收后,仍然無內存空間容納新的Java對象的情況。
翻譯成為更簡單的語言:
- 內存首先在Eden中分配;
- Eden中滿了之后,挪到Survivor Space,清空Eden;
- Survivor Space滿了之后,挪到Old Generation;
- 如果這三個區域都滿了,內存溢出OutOfMemory。
那上面這個過程如何得知呢?今天我就用JConsole給大家演示一下如何看GC的過程。
JConsole介紹
先介紹一下JConsole。
JConsole是什么
JConsole 是一個內置 Java 性能分析器,可以從命令行或在 GUI shell 中運行。您可以輕松地使用 JConsole(或者,它更高端的 “近親” VisualVM )來監控 Java 應用程序性能和跟蹤 Java 中的代碼。
如何啟動JConsole
JConsole是一個程序,在windows里面找到JConsole.exe,雙擊啟動即可。啟動之后的界面如下圖所示:
可以看到,JConsole即可以連接本地進程,也可以連接遠程進程。
應用JConsole學習GC
那么,如何用JConsole學習GC的過程呢?我們首先要設計一個程序,這個程序一直保持內存增長,直到發生內存泄漏。在這個過程中,我們應該JConsole觀察GC的過程。
寫一個內存泄漏的程序
寫一個內存泄漏的程序比較簡單,讀一個很大的文件到內存中,直至內存溢出,代碼如下:
package com.chzhao.test;import java.io.BufferedReader;import java.io.FileReader;import java.io.IOException;import java.util.ArrayList;import java.util.List;public class StringTest { public static void main(String[] args) throws InterruptedException, IOException { List<String> list = new ArrayList<String>(); String file = "d:/wisdombud-unicom.log.2014-12-05"; BufferedReader in = null; in = new BufferedReader(new FileReader(file)); while (true) { String lineMsg = in.readLine(); if (lineMsg == null || lineMsg.equals("")) { break; } list.add(lineMsg); Thread.sleep(10); System.out.println(list.size()); } in.close(); } }
這個文件是我程序的一個日志文件,足夠大,300M+
設置啟動參數
除了寫程序之外,為了快點出現內存泄漏,我們把啟動內存調小。在Eclispse里面調VM arguments就可以,內容為:
-Xms4M -Xmx4M
我設置了4M。
通過JConsole觀察
首先把程序運行起來,在通過JConsole連接到程序上。
因為我們只關心內存,切換到內存標簽頁。有一個下拉圖表,可以觀察不同的內存情況。右下角有個柱狀圖,顯示的是堆內存和棧內存的占用情況。其中堆內存包括Eden Space、Survivor Space和Tenured Gen。如下圖所示:
可以看到,隨著程序的運行,Eden Space會逐漸變滿,到100%之后,Eden Space會變成0%,Survivor Space會變大;Survivor Space變100%之后,會挪到Tenured Gen中,Survivor Space變0%。這個過程和上面講到的GC過程是一樣的,很直觀。
也可以觀察上面的曲線圖,Eden Space的圖是類似波形圖,每次到波谷都是進行了一次GC。Tenured Gen則是類似梯田,一直向上漲,直到內存溢出。如下兩張圖所示。
Eden Space的波形圖
Tenured Gen的波形圖
在VM摘要標簽頁,能看到更多JVM的信息,可以看到分配的內存已經等于堆的最大值了,所以內存溢出。(為什么堆的最大值是5942Kb我沒搞明白,不應該小于4M嗎?如果哪位懂請指點一下。)
結論
通過JConsole工具學習GC的過程,生動形象,很容易明白GC是如何進行的。然而這些知識都屬于GC的基礎知識,如果要了解更多GC的知識,還要多學習。
參考: