JVM各區溢出分析

閱讀文本大概需要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唯一不會發生內存溢出的區域。

推薦閱讀

Spring Boot 最流行的 16 條實踐

SSM框架的面試常見問題

【分佈式】緩存穿透、緩存雪崩,緩存擊穿解決方案

阿里P7給出的一份超詳細 Spring Boot 知識清單

關注我每天進步一點點

你點的每個在看,我都認真當成了喜歡

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章