1. 程式人生 > >Java 記憶體區域與記憶體溢位異常(三)

Java 記憶體區域與記憶體溢位異常(三)

實戰:OutOfMemoryError 異常

參考:《深入理解Java虛擬機器》-jvm高階特性與最佳實現(周志明著)

之前的兩篇中介紹Java虛擬機器中各個執行時記憶體區域的作用,這節中通過人為異常的方式驗證各個執行時區儲存的內容

一、Java堆溢位

Java堆中用於儲存物件的例項,所以只要不斷建立物件,並且保證GC Roots到物件之間有可達路徑(保證物件有引用,而不會被GC回收)來避免垃圾回收機制清除這些物件。那麼在數量達到最大堆容量的限制後就會產生記憶體溢位。

import java.util.ArrayList;
import java.util.List;

/**
 * 測試堆記憶體溢位
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\app\file\java-log\
 *
 * 啟動引數設定Java堆大小為20m,不可擴充套件,通過設定-XX:HeapDumpOnOutOfMemoryError 引數
 * 可以讓虛擬機器在出現記憶體溢位時Dump出當前的記憶體堆轉儲存快照
 */
public class HeapOOM {
    static class OOMObject{

    }
    public static void main(String[] args) {
        // list 引用保證GC Roots 到物件之間有可達路徑
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true){
            list.add( new OOMObject());
            System.out.println("list中添加了"+list.size()+"個物件");
        }
    }
}



// 異常堆疊
……
list中添加了810325個物件
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7576.hprof ...
Heap dump file created [28502312 bytes in 0.088 secs]
Disconnected from the target VM, address: '127.0.0.1:53242', transport: 'socket'
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at com.xiaozhameng.jvm.HeapOOM.main(HeapOOM.java:21)

當出現Java堆記憶體溢位時,異常堆疊資訊java.lang.OutOfMemoryError: 會跟著進一步提示:Java heap space

要解決這個區域的異常,一般的手段是先通過記憶體映象分析工具對Dump出來的堆轉儲存快照進行分析,重點是要確認堆記憶體中的物件是否是必要的,也就是要確定到底出現了記憶體洩漏還是記憶體溢位

可以使用堆轉存快照分析工具(如MAT)進行分析,定位記憶體溢位的問題所在,這裡不再贅述,後面有時間再羅列

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

1、如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverFlowError 異常

2、如果虛擬機器在擴充套件時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。

對於第一點,可以通過設定大量的區域性變數,增大本方法中的本地變量表的長度,最終丟擲StackOverFlowError。

可以通過嘗試不斷建立執行緒的方式使得虛擬機器棧發生OutOfMemoryError異常,但是這樣產生的記憶體溢位與棧空間是否足夠大沒有任何聯絡,或者更加準確地說,在這種情況下,為每個執行緒的棧分配的記憶體越大,反而越容易發生記憶體溢位。

其原因是作業系統分配給每個程序的記憶體時有限制的,虛擬機器提供了引數來控制Java堆和方法區的這兩部分記憶體的最大值。剩餘的記憶體為作業系統記憶體減去Xmx(最大堆記憶體),再減去最大方法區容量(MaxPermSize),程式計數器的記憶體消耗很小,可以忽略。如果虛擬機器程序耗費的記憶體不算在內。剩下的記憶體就由虛擬機器棧和本地方法棧瓜分,每個執行緒分配到的容量越大,可以建立的執行緒數就越少。

這一點需要特別注意,如果是因為建立過多執行緒導致記憶體棧記憶體溢位,在不能減少執行緒數或者更換64位虛擬機器的情況下,就只能通過減少最大堆記憶體。或者減少棧容量來獲取更多的執行緒。

package com.xiaozhameng.jvm;

import org.junit.Test;

/**
 * Java 虛擬機器棧和本地方法棧溢位測試
 *
 * 在HotSpot虛擬機器中,虛擬機器棧和本地方法棧合二為一,因此對於HotSpot虛擬來說,棧容量的大小設定只取決於 -Xss引數設定,關
 * 於虛擬機器棧和本地方法棧,Java虛擬機器規範中定義了兩種異常
 * 1、如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverFlowError 異常
 * 2、如果虛擬機器在擴充套件時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。
 */
public class JavaVMStackOFE {

    private int stackLenth = 1;

    /**
     * VM Agars : -Xss128k
     * 如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverFlowError 異常
     *
     * ---------- 異常堆疊
     * 方法中已經有935個區域性變數
     * Disconnected from the target VM, address: '127.0.0.1:57839', transport: 'socket'
     *
     * java.lang.StackOverflowError
     * 	at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
     * 	at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)
     * 	at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)
     * 	at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
     * 	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:669)
     * 	at java.io.PrintStream.println(PrintStream.java:806)
     * 	at com.xiaozhameng.jvm.JavaVMStackOFE.testStack_StackOverFlowError(JavaVMStackOFE.java:24)
     *
     */
    @Test
    public void testStack_StackOverFlowError(){
        stackLenth ++;
        System.out.println("方法中已經有"+stackLenth+"個區域性變數");
        testStack_StackOverFlowError();
    }

    /**
     * 空方法
     */
    private void dontStop(){
        while (true){
        }
    }

    /**
     * 如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverFlowError 異常
     */
    public void testStack_OutOfMemoryError(){
        while (true){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    /**
     * 執行該方法有較大的風險,可能會導致作業系統假死
     * @param args
     */
    public static void main(String[] args) {
        new JavaVMStackOFE().testStack_OutOfMemoryError();
    }
}

三、方法區和執行時常量池溢位

方法區存放的Class的相關資訊,如類名,訪問修飾符,常量池,欄位描述,方法描述等。關於這個區域的測試,思路是執行時產生大量的類去填滿方法區

執行時常量池屬於方法區的一部分,關於這個區域的測試,可以藉助String.intern() 方法,它的作用是,如果字串常量池中已經包含此字串,則返回常量池中這個字串物件;否則將此String字串包含的物件新增到常量池中並返回物件的引用。

四、本機直接記憶體溢位:DirectMemory容量的設定可以通過-XX:MaxDirectMemorySize 設定,若不指定,則預設跟堆最大記憶體一直。