1. 程式人生 > 其它 >JVM記憶體溢位分析實戰

JVM記憶體溢位分析實戰

概述

jvm中除了程式計數器,其他的區域都有可能會發生記憶體溢位

記憶體溢位是什麼?

當程式需要申請記憶體的時候,由於沒有足夠的記憶體,此時就會丟擲OutOfMemoryError,這就是記憶體溢位

記憶體溢位和記憶體洩漏有什麼區別?

記憶體洩漏是由於使用不當,把一部分記憶體“丟掉了”,導致這部分記憶體不可用。
當在堆中建立了物件,後來沒有使用這個物件了,又沒有把整個物件的相關引用設為null。此時垃圾收集器會認為這個物件是需要的,就不會清理這部分記憶體。這就會導致這部分記憶體不可用。
所以記憶體洩漏會導致可用的記憶體減少,進而會導致記憶體溢位

用到的jvm引數

下面為了說明溢位的情景,會執行一些例項程式碼,同時需要給jvm指定引數

  • -Xms 堆最小容量(heap min size)
  • -Xmx 堆最大容量(heap max size)
  • -Xss 棧容量(stack size)
  • -XX:PermSize=size 永生代最小容量
  • -XX:MaxPermSize=size 永生代最大容量

堆溢位

堆是存放物件的地方,那麼只要在堆中瘋狂的建立物件,那麼堆就會發生記憶體溢位。


下面做一個堆溢位的實驗
執行這段程式碼的時候,要給jvm指定引數

//jvm引數:-Xms20m -Xmx20m
public class HeapOOMTest {
	public static void main(String[] args){
		LinkedList<HeapOOMTest> l=new LinkedList<HeapOOMTest>();//作為GC Root
		while(true){
			l.add(new HeapOOMTest());//瘋狂建立物件
		}
	}
}

-Xms20m -Xmx20m作用是將jvm的最小堆容量和最大堆容量都設定為20m,這樣就不會動態擴充套件jvm堆
這段程式碼瘋狂的建立物件,雖然物件沒有宣告變數名引用,但是將物件新增到佇列l中,這樣佇列l就持有了一份物件的引用
通過可達性演算法(jvm判斷物件是否可被收集的演算法)分析,佇列l作為GC Root,每一個物件都是l的一個可達的節點,所以瘋狂建立的物件不會被收集,這就是記憶體洩漏,這樣總有一天堆就溢位了。


執行結果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.LinkedList.linkLast(Unknown Source)
	at java.util.LinkedList.add(Unknown Source)
	at test.HeapOOMTest.main(HeapOOMTest.java:23)

程式發生記憶體溢位,並提示發生在Java heap space

分析解決方法

思路

visualVM工具分析堆快照
如果發生記憶體洩漏
step1:找出洩漏的物件
step2:找到洩漏物件的GC Root
step3:根據洩漏物件和GC Root找到導致記憶體洩漏的程式碼
step4:想法設法解除洩漏物件與GCRoot的連線
如果不存在洩漏:

  1. 看下是否能增大jvm堆的最大容量
  2. 優化程式,減小物件的生命週期

前期準備

當發生堆溢位的時候,可以讓程式在崩潰時產生一份堆記憶體快照
產生堆記憶體快照的方法:
給jvm加上引數XX:+HeapDumpOnOutofMemoryError,這樣就會在程式崩潰的時候,產生一份堆記憶體快照
分析堆記憶體快照我建議用jdk自帶的視覺化監視工具visualVM,位置在jdk安裝目錄下的bin,如果是在linux環境的話,可以把快照傳到window。因為分析工具會佔用很大的記憶體,不建議在服務端進行分析。

實戰

下面對剛才程式產生的堆記憶體快照進行分析。
開啟visualVM,裝入剛剛生成的快照,開啟類標籤頁

佇列和瘋狂建立的物件幾乎佔滿了整個棧,想要讓垃圾收集器回收這些物件,要讓他們與GC Root斷開連線
雙擊HeapOOMTest類,跳轉到例項標籤頁,可以檢視這個類的所有例項
在例項上右鍵——顯示最近的垃圾回收根節點,可以看到這個物件與根節點的連線

只要斷開HeapOOMTest物件與LinkedList的連線,這些瘋狂建立的物件就會被收集了

棧溢位

呼叫方法的時候,會在棧中入棧一個棧幀,如果當前棧的容量不足,就會發生棧溢位StackOverFlowError
那麼只要瘋狂的呼叫方法,並且有意的不讓棧幀出棧就可以導致棧溢位了。


下面來一次棧溢位

//jvm引數:-Xss128k
public class StackSOFTest {
	public void stackLeak(){
		stackLeak();//遞迴,瘋狂的入棧,有意不讓出棧
	} 
	public static void main(String[] args){
		StackSOFTest s=new StackSOFTest();
		s.stackLeak();
	}
}

jvm設定引數-Xss128k,目的是縮小棧的空間,這樣棧溢位“來的快一點”
程式中用了遞迴,讓棧幀瘋狂的入棧,又不讓棧幀出棧,這樣就會棧溢位了。


執行結果:

Exception in thread "main" java.lang.StackOverflowError
	at test.StackSOFTest.stackLeak(StackSOFTest.java:17)
	at test.StackSOFTest.stackLeak(StackSOFTest.java:17)

執行時常量池溢位

這裡儲存的是一些常量、字面量。如果執行時常量池記憶體不足,就會發生記憶體溢位。從jdk1.7開始,執行時常量池移動到了堆中,所以如果堆的記憶體不足,也會導致執行時常量池記憶體溢位。


下面來一次執行時常量池溢位,環境是jdk8
只要建立足夠多的常量,就會發生溢位

/**
 * jvm引數:
 * jdk6以前:-XX:PermSize=10M -XX:MaxPermSize=10M
 * jdk7開始:-Xms10m -Xmx10m
 * */
public class RuntimePoolOOM {
	public static void main(String[] args){
		int i=1;
		LinkedList<String> l=new LinkedList<String>();//保持常量的引用,防止被fullgc收集
		while(true){
			l.add(String.valueOf(i++).intern());//將常量新增到常量池
		}
	}
}

因為jdk6以前,執行時常量池是在方法區(永生代)中的,所以要限制永生代的容量,讓記憶體溢位來的更快。
從jdk7開始,執行時常量池是在堆中的,那麼固定堆的容量就好了
這裡用了連結串列去儲存常量的引用,是因為防止被fullgc清理,因為fullgc會清理掉方法區和老年代
intern()方法是將常量新增到常量池中去,這樣執行時常量池一直都在增長,然後記憶體溢位


執行結果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.lang.Integer.toString(Unknown Source)
	at java.lang.String.valueOf(Unknown Source)
	at test.RuntimePoolOOM.main(RuntimePoolOOM.java:30)

提示在heap區域發生記憶體溢位,果然執行時常量池被移到了堆中

方法區溢位

方法區是存放類的資訊,而且很難被gc,只要載入了大量類,就有可能引起方法區溢位
這裡將不做演示了,想試試的可以用cglib建立大量的代理類

分析

工作中也有可能會遇上方法區溢位:
當多個專案都有相同jar包的時候,又都存放在WEB-INF\lib\下,這樣每個專案都會載入一遍jar包。會導致方法區中有大量相同類(被不同的類載入器所載入),又不會被gc掉。

解決方案:

    1. 在應用伺服器中建立一個共享lib庫,把專案中常用重複的jar包存放在這裡,專案從這裡載入jar包,這樣就會大大減少類載入的數量,方法區也“瘦身”了

    2. 如果實在不能瘦身類的話,那可以擴大方法區的容量,給jvm指定引數-XX:MaxPermSize=xxxM