上一節介紹了針對JVM的監控工具,包括JPS可以查看當前所有的java進程,jstack查看線程棧可以幫助你分析是否有死鎖等情況,jmap可以導出java堆文件在MAT工具上進行分析等等。這些工具都非常有用,但要用好他們需要不斷的進行實踐分析。本文將介紹使用MAT工具進行java堆分析的案例。
內存溢出(OOM)的原因
我們常見的OOM(OutOfMemoryError)發生的原因不只是堆內存溢出,堆內存溢出只是OOM其中一種情況,OOM還可能發生在元空間、線程棧、直接內存。
下面演示在各個區發生OOM的情況:
堆OOM
public static void main(String[] args) { List<Byte[]> list=new ArrayList<Byte[]>(); for(int i=0;i<100;i++){ //構造1M大小的byte數值 Byte[] bytes=new Byte[1024*1024]; //將byte數組添加到list列表中,因為存在引用關系所以bytes數組不會被GC回收 list.add(bytes); } }
以上程序設置最大堆內存50M,執行:
顯然程序通過循環將占用100M的堆空間,超過了設置的50M,所以發生了堆內存的OOM。
針對這種OOM,解決辦法是增加堆內存空間,在實際開發中必要的時候去掉引用關系,使垃圾回收器盡快對無用對象進行回收。
元空間OOM
public static void main(String[] args) throws Exception { for(int i=0;i<1000;i++){ //動態創建類 Map<Object,Object> propertyMap = new HashMap<Object, Object>(); propertyMap.put("id", Class.forName("java.lang.Integer")); CglibBean bean=new CglibBean(propertyMap); //給 Bean 設置值 bean.setValue("id", new Random().nextInt(100)); //打印 Bean的屬性id System.out.println("id=" + bean.getValue("id")); } }
以上代碼通過Cglib動態創建class,設置元數據區大小為4M:
由於代碼循環創建class,大量的class元數據,存放在元數據區超過了設置的4M空間,因此報元數據區OOM:
解決該OOM的辦法是增大MaxMetaspaceSize參數值,或者干脆不設置該參數,在默認情況元空間可使用的內存會受到本地內存的限制。
棧OOM
當創建新的線程時JVM會給每個線程分配棧內存,當創建線程過多,占用的內存也就越多,這種情況下有可能發生OOM:
public static void main(String[] args) throws Exception { //循環創建線程 for (int i = 0; i < 1000000; i++) { new Thread(new Runnable() { public void run() { try { //線程sleep時間足夠長,保證線程不銷毀 Thread.sleep(200000000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); System.out.println("created " + i + "threads"); } }
很明顯解決此OOM的辦法是減小線程數。
直接內存OOM
public static void main(String[] args) throws Exception { for (int i = 0; i < 1000000; i++) { //申請堆外內存,這個內存是本地的直接內存,並非java堆內存 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024*1024*1024); System.out.println("created "+i+" byteBuffer"); } }
ByteBuffer的allocateDirect方法可以申請直接內存,當申請的內存超過的本地可用內存時,會報OOM:
解決該OOM的辦法是適當使用堆外內存,如有必要可顯式執行垃圾回收。(即在代碼中執行System.gc();)
MAT工具使用
當java應用出現故障時,我們可能需要使用MAT分析問題,找出問題出現的原因,下面通過一個案例介紹MAT的使用方法:
准備:
我們事先從程序運行環境上使用jmap工具或者jvisualvm導出一個堆快照文件出來。
使用MAT工具打開:
我們發現占用內存最大的對象是AppClassLoader,我們知道AppClassLoader是用來加載應用的類,因此我們進一步查看它引用的對象。
下圖顯示了AppClassLoader引用的對象空間使用情況,“Shallow Heap”表示淺堆的大小,淺堆就是類自身所占用的空間大小,也就是類本身元數據的大小。“Retained Heap”表示深堆的大小,深堆表示該類以及它引用的其他類所占用空間的總和,也表示該類被垃圾回收后,所能夠釋放的空間大小。(如果該類被回收了,他引用的對象會變成不可達對象因此也會被回收)
我們隨藤摸瓜,繼續查看深堆占用最大的對象。
從上圖可以看出造成深堆比較大的原因是程序當中包含了一個ArrayList,他里面包含有大量的String對象,並且每個String對象有80216字節大小。
因此針對這個堆的分析基本清楚了,因為程序中包括大量的String對象,而他們又在ArrayList當中,引用關系一直存在,因此無法被垃圾回收,造成OOM。
MAT其他功能說明
除了上述我們使用到的MAT功能外,還有一些功能也是經常用到的。
Histogram:顯示每個類使用情況以及占用空間大小。
上圖可以看到char[]類,有1026個對象,占用5967480字節的空間,通過上面的分析得出結論是String對象占用了大部分的空間,而Stirng對象內部存放字符使用char[]來存放的,所以這里顯示char[]的淺堆大小為5967480字節也是可以理解的。
Thread_overview:顯示線程相關的信息。
OQL:通過類似SQL語句的表達式查詢對象信息。
上圖通過OQL語句查詢字符串中匹配123的String對象。
結語
本文首先介紹了java程序中出現OOM的幾種情況,然后通過簡單的案例介紹了MAT的基本用法。






![K4Q7WUYB6Q8}]V7Q55$CCC9 K4Q7WUYB6Q8}]V7Q55$CCC9](/image/aHR0cHM6Ly9pbWFnZXMyMDE3LmNuYmxvZ3MuY29tL2Jsb2cvMzUyNTExLzIwMTcwOS8zNTI1MTEtMjAxNzA5MTIxMTM4Mjk0ODUtMTk0MzczMzQ2OS5wbmc=.png)
![FW)8Z)LWY@$6COXYT]L@S)5 FW)8Z)LWY@$6COXYT]L@S)5](/image/aHR0cHM6Ly9pbWFnZXMyMDE3LmNuYmxvZ3MuY29tL2Jsb2cvMzUyNTExLzIwMTcwOS8zNTI1MTEtMjAxNzA5MTIxMTM4MzAyMTktMjgzMzU0NjUyLnBuZw==.png)

![@(}%4RVBY%V]4EU[{0_2S%2 @(}%4RVBY%V]4EU[{0_2S%2](/image/aHR0cHM6Ly9pbWFnZXMyMDE3LmNuYmxvZ3MuY29tL2Jsb2cvMzUyNTExLzIwMTcwOS8zNTI1MTEtMjAxNzA5MTIxMTM4MzE3ODItMzkzNTE0NTM2LnBuZw==.png)
![{{7I%O]JG5SMRPDK5AS7ZV0 {{7I%O]JG5SMRPDK5AS7ZV0](/image/aHR0cHM6Ly9pbWFnZXMyMDE3LmNuYmxvZ3MuY29tL2Jsb2cvMzUyNTExLzIwMTcwOS8zNTI1MTEtMjAxNzA5MTIxMTM4MzI1OTQtMTg3NzA5NjY3OC5wbmc=.png)
UT5](BIZLTYH9O_GR](/image/aHR0cHM6Ly9pbWFnZXMyMDE3LmNuYmxvZ3MuY29tL2Jsb2cvMzUyNTExLzIwMTcwOS8zNTI1MTEtMjAxNzA5MTIxMTM4MzMzNDQtMjA3NDA2OTY0NC5wbmc=.png)

