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 設定,若不指定,則預設跟堆最大記憶體一直。