閱讀文本大概需要3分鐘。
0x01:Java虛擬機棧和本地方法棧溢出
由於在Hotspot虛擬機中中不區分虛擬機棧和本地方法棧,因此通過-Xoss修改參數是無效的,可以通過修改-Xss設定。
如果線程請求的棧深度大於虛擬機允許的最大深度,將拋出StackOverflowError異常。
如果虛擬機在擴展棧時無法申請到足夠的內存空間,將拋出OutOfMemoryError異常。
這兩種異常有一些重疊的部分:當棧空間無法繼續分配時,到底是內存太小,還是已經使用的棧空間過大,其本質只是對同一件事情的兩種不同描述。
可以通過以下方法驗證:
在使用-Xss參數減少棧內存容量,結果拋出Stack OverflowError異常,異常出現時輸出的堆棧深度相應縮小。
定義了大量的本地變量,增大此方法棧中本地變量表的長度,結果拋出Stack OverflowError異常時輸出的堆棧深度相應縮小。
可以通過遞歸調用的方式進行測試:
public void stackLeak() {
stackLeak();
}
通過不斷建立線程的方式可以生產內存異常異常,但是產生的內存異常異常和棧空間是否足夠大並不存在任何關聯,在這種情況下,爲每個線程的棧分配的內存越大,反而越容易產生內存溢出。
操作系統爲虛擬機分配的內存是有限制的,如果虛擬機進程本身消耗的內存計算在內,剩餘的內存就由虛擬機棧和本地方法棧瓜分了,每個線程分配到的棧容量越大,可以建立的線程數量自然就越少,建立線程時就越容易把剩下的內存耗盡。
如果是建立線程過多導致內存溢出,在不能減少線程數量或者更換64位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。
可以通過死循環創建線程的方式模擬“由於線程過多導致的內存溢出”:
while(true){
Thread t = new Thread(new Runable(){
......
});
}
0x02:Java堆內存溢出
可以通過不停的創建對象來造成堆內存溢出
public static void main(String[] args) {
List list = new ArrayList<>();
while(true) {
list.add(new ObjectBIg())
}
}
使用-XX:+HeapDumpOnOutOfMemoryError可以在虛擬機在出現內存溢出異常時Dump出當前的內存堆轉存儲快照以便後續進行分析。
對Dump快照進行分析,需要區分出到底是內存泄漏Memory Leak還是內存異常Memory Overflow。
如果是內存泄漏,進一步通過工具對GC Root的引用鏈進行分析。
如果不是內存泄漏,就是內存中的對象確實都還必須存活,那就應該修改虛擬機參數Xmx Xms,同時判斷是否可以通過調大物理內存的方式解決。然後從代碼角度檢測是否存在某些對象生命週期過長、持有狀態時間過長的情況,嘗試減少程序運行期的消耗。
0x03: 方法區和運行時常量池溢出
由於運行時常量池屬於方法區的一部分,因此兩個區域放在一塊執行。
String.intern()是一個Native方法,它的作用是如果字符串常量池中已經包含了此String對象的字符串,則返回代表池中這個字符串的String對象;否則將此String對象包含的字符串添加到常量池中,並且返回此String對象的引用。
可以通過以下代碼測試運行時常量池溢出:
public class Test {
public static void main(String[] args) {
int i =0;
List<String> list = new ArrayList();
while(true) {
list.add(String.valueOf(i++).intern());
}
}
}
可以在拋出的異常後面發現“Perm space”信息。
可以使用String.intern()測試運行時常量池:
public class Test1 {
public static void main(String[] args) {
String str1 = new StringBuilder("111").append("-222").toString();
System.out.println(str1.intern()==str1);
String str2= new
StringBuilder("jav").append("a").toString();;
System.out.println(str2.intern()==str2);
}
}
結果:
true
false
JDK1.7中的intern實現不會複製實例,只是在常量池中記錄首次出現的實例引用,因此intern()返回的引用和由StringBuilder創建的那個字符串實例是同一個。
對str2比較返回false是因爲“java”字符串在執行StringBuilder.toString()之前已經出現過了,字符串常量池中已經有它的引用了,不符合“首次出現”的原則。
方法區用於存放Class相關的信息,如類名、訪問修飾符、常量池、字段描述、方法描述等,對於這些區域的測試,基本的思路是運行時產生大量的類填充方法區,直到溢出。
可以藉助GCLib直接操作字節碼運行時產生大量的動態類:
public class Test1 {
public static void main(final String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMOBject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(objects,args);
}
});
enhancer.create();
}
}
static class OOMOBject{
}
}
除了GCLib字節碼增強和動態語言之外,常見的還有大量JSP或者動態生成JSP文件的應用、基於OSGi的應用等
另外:程序計數器是JVM唯一不會發生內存溢出的區域。
推薦閱讀
關注我每天進步一點點
你點的每個在看,我都認真當成了喜歡