模擬Java內存溢出

本文通過修改虛擬機啟動參數,來剖析常見的java內存溢出異常(基於jdk1.8)。

修改虛擬機啟動參數

這裡我們使用的是IDEA集成開發環境,選擇Run/Debug Configurations

模擬Java內存溢出

然後選擇Configuration,修改VM options配置,就可以修改虛擬機啟動參數了,本文的示例代碼doc註釋部分將會給出需要設置的虛擬機參數。

模擬Java內存溢出

Java堆溢出

<code> 1import java.util.ArrayList;
2import java.util.List;
3
4/**
5 * 堆溢出測試.
6 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
7 * -XX:HeapDumpPath=/Users/lijl/Desktop
8 *
9 * @author jialin.li
10 * @date 2020-04-08 10:02
11 */
12public class HeapOOM {
13 public static void main(String[] args) {
14 List<heapoom> list = new ArrayList<>();
15
16 while (true) {
17 list.add(new HeapOOM());
18 }
19 }
20}
/<heapoom>/<code>

這裡簡單解釋一下代碼,我們通過 -xms20m -Xmx20m 兩個參數,限制了Java堆的大小為20MB,不可擴展,後兩個參數控制了當出現了OutOfMemoryError時,會Dump出當前內存的 堆轉儲快照 ,並保存到指定位置中。

接下來我們可以使用jdk自帶的VisualVM來打開快照文件。

命令行輸入 jvisualvm ,點擊左上角的 裝入 ,選中我們dump出來的堆快照文件。

模擬Java內存溢出

經過重新加載的堆內存記錄如下:

模擬Java內存溢出

這裡可以很直觀的看出,OutOfMemoryError產生的原因,是HeapOOM這個對象導致的。

解決問題的思路是:首先我們要排除 內存洩露 ,即我們不需要的對象沒有被回收掉。我們要找到 洩漏的對象是如何與GC Root進行關聯的? 從而準確定位出洩漏代碼的位置,然後進行修改。

如果不是內存洩漏,即堆中的對象必須存活,這個時候,我們可以通過調節虛擬機的堆參數(-Xms -Xmx),適當調大堆內存。但是在此之前,我們一定要檢查一下 代碼是否存在優化的空間

,如:是否存在某些對象的生命週期過長?是否可以使用享元模式減少對象數量?等等

虛擬機棧溢出

<code> 1/**
2 * 棧溢出測試.
3 * VM Args: Xss128k
4 *
5 * @author jialin.li
6 * @date 2020-04-08 10:02
7 */
8public class StackSOF {
9
10 private static int stackLength = 1;

11
12 public static void main(String[] args) {
13 stackLeak();
14 }
15
16 private static void stackLeak(){
17 try{
18 stackLength++;
19 stackLeak();
20 }catch (StackOverflowError e){
21 System.out.println("stack Length:" +stackLength);
22 e.printStackTrace();
23 }
24 }
25}
/<code>

StackOverflowError屬於比較好排查的一種錯誤,有錯誤棧可以閱讀,大部分出現這種錯誤,都是遞歸程序書寫的問題,沒有弄清楚什麼時候需要return;結束遞歸。

模擬Java內存溢出

這裡有一個有趣的現象,操作系統給每個進程分配的內存是有限的,在多線程的場景下,如果每個線程分配的棧內存過大,就會導致OOM,這個時候可以適當減少每個線程的棧內存,來解決溢出問題(這可能不是最好的辦法,只是因為這是一種比較不符合直覺的解決問題方式,所以這裡單獨說一下)。

方法區溢出

方法區用來存放Class相關的信息,比如類名、訪問修飾符、常量池、字段描述等等。我們可以用運行時產生的大量的類填滿方法區,這裡我們使用了gclib來操作字節碼,maven座標如下:

<code>1
2<dependency>
3 <groupid>cglib/<groupid>
4 <artifactid>cglib/<artifactid>
5 <version>2.2.2/<version>
6/<dependency>
/<code>

代碼:

<code> 1import net.sf.cglib.proxy.Enhancer;
2import net.sf.cglib.proxy.MethodInterceptor;
3
4/**
5 * 方法區溢出測試.
6 * VM Args: -XX:PermSize=10k -XX:MaxPermSize=10k
7 *
8 * @author jialin.li
9 * @date 2020-04-08 14:18
10 */

11public class JavaMethodAreaOOM {
12 public static void main(String[] args) {
13 while (true){
14 Enhancer enhancer = new Enhancer();
15 enhancer.setSuperclass(OOMObject.class);
16 enhancer.setUseCache(false);
17 enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o, args));
18 }
19 }
20
21 static class OOMObject{
22
23 }
24}
/<code>

如果你用的也是和我一樣的jdk1.8,此時我們將沒有辦法得到OOM,因為在jdk1.8之後,PermGen已經被移除了,所以永久代的參數也被一同移除。方法區的 靜態變量和常量池 併入堆中,而類的 元信息 放到元空間中,元空間是一塊本地內存,所以它的最大可分配空間就是系統內存的最大可用空間。

我們可以將參數改為:VM Args: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m,再執行上述代碼,發現即使在設置了元空間大小的情況下,仍然不會觸發OOM,可見元空間可以有效解決方法區OOM問題(會觸發元空間的垃圾回收策略)。

本機直接內存溢出

這種情況發生的比較少,直接內存的容量,我們可以通過-XX:MaxDirectMemorySize來指定,如果不指定,默認與堆的最大值一樣。我們可以通過Unsafe提供的方法申請本地內存分配。

<code> 1import sun.misc.Unsafe;
2import java.lang.reflect.Field;
3
4/**
5 * 本機內存溢出.
6 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
7 *
8 * @author jialin.li
9 * @date 2020-04-08 15:53
10 */
11public class DirectMemoryOOM {
12 private static final int _1MB = 1024 * 1024;
13
14 public static void main(String[] args) throws IllegalAccessException {
15 Field unsafeField = Unsafe.class.getDeclaredFields()[0];
16 unsafeField.setAccessible(true);
17 Unsafe unsafe = (Unsafe) unsafeField.get(null);
18 while (true) {
19 unsafe.allocateMemory(_1MB);
20 }
21 }
22}
/<code>

如果你是IDE集成開發環境,可能會因為內存不足結束執行程序。

<code>1Process finished with exit code 137 
/<code>

來源:https://www.tuicool.com/articles/yiQnyq3


分享到:


相關文章: