Java記憶體溢位

大雄45發表於2021-03-20
導讀 記憶體溢位是指應用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式執行要用到的記憶體大於虛擬機器能提供的最大記憶體。這篇文章整理自《深入理解java虛擬機器》。
一、記憶體溢位原因

記憶體溢位就是記憶體不夠,引起記憶體溢位的原因有很多種,常見的有以下幾種:

  1. 1、記憶體中載入的資料量過於龐大,如一次從資料庫取出過多資料;
  1. 2、集合類中有對物件的引用,使用完後未清空,使得JVM不能回收;
  1. 3、程式碼中存在死迴圈或迴圈產生過多重複的物件實體;
  1. 4、使用的第三方軟體中的BUG;
  1. 5、啟動引數記憶體值設定的過小;

當然實際情況中記憶體溢位的原因就太多了。下面我們就對這些原因分類一下:

以上的圖是基於java7來敘述的,從上面這張圖我們能夠得到如下資訊:java虛擬機器把記憶體分為5個模組。

  1. (1)程式計數器:

程式計數器是執行緒私有的,主要的作用是透過改變這個計數器的值來選取下一條需要執行的位元組碼指令。既然每個執行緒都有一個,那麼這些執行緒的計數器是互不影響的。也不會丟擲任何異常。

  1. (2)虛擬機器棧和本地方法棧:

虛擬機器棧描述的是java方法執行的記憶體模型,每個方法在執行的時候都會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連線、方法出口等資訊。本地方法棧與虛擬機器棧的區別是,虛擬機器棧為虛擬機器執行java方法服務,而本地方法棧則為虛擬機器提供native方法服務。

在單執行緒的操作中,無論是由於棧幀太大,還是虛擬機器棧空間太小,當棧空間無法分配時,虛擬機器丟擲的都是StackOverflowError異常,而不會得到OutOfMemoryError異常。而在多執行緒環境下,則會丟擲OutOfMemoryError異常。

  1. (3)java堆和方法區:

java堆區主要存放物件例項和陣列等,方法區儲存類資訊、常量、靜態變數等等。執行時常量池也是方法區的一部分。這兩塊區域是執行緒共享的區域,只會丟擲OutOfMemoryError。

不知道各位在B站看見過那個面試經典場景沒,在回答java的記憶體執行資料區結構時,以上的功能作用是一方面,如果回答時把記憶體溢位問題新增上是一個極大的加分項。

二、記憶體溢位例項
1、堆溢位

既然堆是存放例項物件的,那我們就無線建立例項物件。這樣堆區遲早會滿。

public class HeapOOM { 
    static class User {} 
 public static void main(String[] args) { 
   Listlist = new ArrayList(); 
         while (true) { 
             list.add(new User()); 
      } 
 } 
} 
/*Exception in thread "main" java.lang.OutOfMemoryError:  
GC overhead limit exceeded 
 at com.fdd.test.HeapOOM.main(HeapOOM.java:11)*/

因為我提前設定了堆區記憶體,所以無限建立就會丟擲異常。

2、虛擬機器棧和本地方法棧溢位

Java虛擬機器規範中描述了兩種異常:

  1. 如果執行緒請求的棧深度大於虛擬機器鎖允許的最大深度,將丟擲StackOverflowError異常。
  1. 如果虛擬機器在擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。

第一種我們只需要使用方法遞迴呼叫即可模擬:

public class StackOutOfMemoryError { 
    public static void main(String[] args) {      
         test(); 
    } 
    private static void go() { 
        System.out.println("StackOverflowError異常"); 
        test(); 
    } 
} 
/*Exception in thread "main" java.lang.StackOverflowError 
 at sun.nio.cs.ext.DoubleByte$Encoder.encodeLoop(DoubleByte.java:617) 
 at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579) 
 at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271) 
 at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125) 
 at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207) 
 at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129) 
 at java.io.PrintStream.write(PrintStream.java:526) 
 at java.io.PrintStream.print(PrintStream.java:597) 
 at java.io.PrintStream.println(PrintStream.java:736) 
 at com.fdd.test.StackOutOfMemoryError.go(StackOutOfMemoryError.java:11) 
 at com.fdd.test.StackOutOfMemoryError.go(StackOutOfMemoryError.java:13)*/

第二種也可以遞迴呼叫模擬,,但是使用的是類直接呼叫。

public class JavaVMStackSOF { 
    private int stackLength = 1; 
    public void stackLeak() { 
        stackLength++; 
        stackLeak(); 
    } 
 public static void main(String[] args) { 
        JavaVMStackSOF oom = new JavaVMStackSOF(); 
        oom.stackLeak(); 
    } 
} 
/*Exception in thread "main" java.lang.StackOverflowError 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:18) 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) 
   at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) 
   ... */
3、方法區和執行時常量池溢位
public class JavaMethodAreaOOM { 
    public static void main(String[] args) { 
        while (true) { 
            Enhancer enhancer = new Enhancer(); 
            enhancer.setSuperclass(User.class); 
            enhancer.setUseCache(false); 
            enhancer.setCallback(new MethodInterceptor() { 
                public Object intercept(Object obj, Method method, 
                      Object[] args, MethodProxy proxy) throws Throwable { 
                    return proxy.invokeSuper(obj, args); 
                } 
            }); 
            enhancer.create(); 
        } 
    } 
    static class User {} 
} 
/*Exception in thread "main" 
 Exception: java.lang.OutOfMemoryError thrown  
 from the UncaughtExceptionHandler in thread "main" 
*/
4、本機直接記憶體溢位

DirectMemory容量可透過-XX: MaxDirectMemorySize指定,如果不指定,則預設與Java堆最大值 (-Xmx指定)一樣。

public class DirectMemoryOOM { 
    private static final int _1MB = 1024 * 1024; 
    public static void main(String[] args) throws Exception { 
        Field unsafeField = Unsafe.class.getDeclaredFields()[0]; 
        unsafeField.setAccessible(true); 
        Unsafe unsafe = (Unsafe) unsafeField.get(null); 
        while (true) { 
            unsafe.allocateMemory(_1MB); 
        } 
    } 
}

上面介紹了幾個例項,那遇到這種問題如何排查呢?

三、記憶體溢位排查

排查其實最主要的就是檢查程式碼,而且記憶體溢位往往都是程式碼的問題。當然一下幾點都是需要注意的:

(1)記憶體中載入的資料量過於龐大,如一次從資料庫取出過多資料;

(2)集合類中有對物件的引用,使用完後未清空,使得JVM不能回收;

(3)程式碼中存在死迴圈或迴圈產生過多重複的物件實體;

(4)使用的第三方軟體中的BUG;

(5)啟動引數記憶體值設定的過小;

最後就是解決了。

第一步,修改JVM啟動引數,直接增加記憶體。

第二步,檢查錯誤日誌

第三步,對程式碼進行走查和分析,找出可能發生記憶體溢位的位置。

一般情況下程式碼出錯的機率會比較大一些,當然了不同的場景不同錯誤總是複雜多樣的。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2764024/,如需轉載,請註明出處,否則將追究法律責任。

相關文章