Java記憶體溢位案例模擬和原理分析過程
在JVM虛擬機器規範中,Java虛擬機器執行時資料區域除了程式計數器(Program Counter Register)外都有可能出現OutOfMemoryError的情況,使用Hotspot虛擬機器簡單的模擬堆疊記憶體溢位的場景,方便快速定位是什麼區域的記憶體溢位。
堆
通過VM引數設定Java堆的大小,避免堆可擴充套件記憶體(設定-Xms和Xmx一樣可避免堆自動擴充套件);
通過設定-XX:+HeapDumpOnOutOf-MemoryError可以讓虛擬機器在出現記憶體溢位異常的時候Dump出當前的記憶體堆轉儲快照。
/** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * @author Vicente * @version 1.0 * @date 2020/4/5 10:28 */ public class TestHeapOOM { public static void main(String[] args) { List<TestHeapOOM> list = new ArrayList<TestHeapOOM>(); while (true) { list.add(new TestHeapOOM()); } } }
設定啟動引數:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
執行結果:
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid3676.hprof ... Heap dump file created [28279988 bytes in 0.099 secs] 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:265) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231) at java.util.ArrayList.add(ArrayList.java:462) at com.oom.TestHeapOOM.main(TestHeapOOM.java:19)
使用IDEA和Eclipse都可以設定啟動時的引數。
堆轉儲快照檔案一般生成後位於你的work space,拿到檔案後要對快照檔案進行分析,可以採用不同的工具來幫助我們分析,這裡推薦兩種:
jhat
在IDEA或者Eclispe的終端控制檯直接輸入命令
jhat java_pid13232.hprof
當dump的檔案過大時需要設定jhat引數:jhat -J-Xmx2048m java_pid13232.hprof,預設-Xmx為1024
對堆快照進行分析
Reading from java_pid13232.hprof...
Dump file created Sun Apr 05 10:54:06 CST 2020Snapshot read,resolving...
Resolving 818818 objects...
Chasing references,expect 163
dots................................................................................................................
Eliminating duplicate
references..........................................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
通過訪問http://localhost:7000即可檢視分析結果,物件記憶體分配的大小等資訊。
mat
工具下載地址:https://eclipse.org/mat/downloads.php,選擇需要下載的版本,windows版本下載後是一個壓縮包,直接解壓執行即可。
開啟需要分析的堆轉儲檔案,分析後會展示一個概要預覽
Leak Suspects » Leaks » Problem Suspect 在這裡面可以看到物件的個數,物件佔用大小等資訊,這裡包含兩個重要資訊
Generally speaking,shallow heap of an object is its size in the heap and retained size of the same object is the amount of heap memory that will be freed when the object is garbage collected.
具體解釋可以參考:https://help.eclipse.org/2020-03/index.jsp
複製程式碼 程式碼如下:- Shallow Heap:某個物件自身大小,不包含其引用物件的大小;- Retained Heap:某個物件在發生GC回收時,如果被釋放,其釋放記憶體的大小,這就要包含其引用的物件佔用堆記憶體的大小。
這裡具體使用就不再描述,可以參考官方文件。
棧
Java執行時資料區包含虛擬機器棧和本地方法棧,在Hotspot虛擬機器實現中對於本地方法棧的引數(-Xoss)設定並無實際效果,只通過-Xss引數來模擬棧的記憶體溢位。《Java虛擬機器規範》中指出:
如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,丟擲StackOverflowError異常如果虛擬機器的棧記憶體允許動態擴充套件,當擴充套件棧容量無法申請到足夠的記憶體時,丟擲OutOfMemoryError異常
設定執行引數VM Args:-Xss128k
/** * VM Args:-Xss128k * @author Vicente * @version 1.0 * @date 2020/4/5 12:39 */ public class TestStackOverflow { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { TestStackOverflow overflow = new TestStackOverflow(); try { overflow.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + overflow.stackLength); throw e; } } }
執行結果:
Exception in thread "main" java.lang.StackOverflowError
at com.oom.TestStackOverflow.stackLeak(TestStackOverflow.java:17)
at com.oom.TestStackOverflow.stackLeak(TestStackOverflow.java:17)
//省略...
stack length:36984
//省略...
根據作業系統的不同和Java虛擬機器版本的不同,棧容量的最小值也會有所不同,改變棧容量的大小或者棧幀過大時都會導致StackOverflowError異常。
Hotspot虛擬機器不支援擴充套件棧記憶體,除非在建立執行緒申請記憶體就不足會導致OutOfMemoryError異常,其他情況都是在執行時因為棧容量無法容納新的棧幀而導致StackOverflowError異常。
不同的虛擬機器實現有著不同的細節處理,其他虛擬機器實現如果是可擴容棧空間,棧容量不足時會丟擲OutOfMemoryError異常,當遇到OutOfMemoryError時要先判斷是棧空間還是堆記憶體的異常。
方法區
在JDK6之前的Hotspot虛擬機器中方法區被設定在永久代中,執行時常量池也屬於方法區的一部分,可以通過-XX:PermSize和-XX:MaxPermSize限制永久代的大小,在JDK7開始逐步的去永久代,到了JDK8就開始使用元空間(meta-space)來實現方法區,儲存程式執行時的資料。
使用JDK6,設定永久代引數-XX:PermSize=2M -XX:MaxPermSize=2M
public class TestConstantPoolOOM { public static void main(String[] args) throws Throwable { //使用Set保持著常量池引用,避免Full GC回收常量池行為 Set<String> set = new HashSet<String>(); // 在short範圍內足以讓6MB的PermSize產生OOM了 short i = 0; while (true) { set.add(String.valueOf(i++).intern()); //String.valueOf(i++).intern(); } } }
執行結果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.oom.TestConstantPoolOOM.main(TestConstantPoolOOM.java from InputFileObject:21)
可以看到OutOfMemoryError後面指明瞭記憶體溢位的位置PermGen space;在JDK8中使用-XX:MaxMeta-spaceSize引數把方法區容量同樣限制,也不會出現異常,因為從JDK7開始常量池已經從永久代移到了Java堆的位置,此時限制Java堆的大小便會丟擲異常,定位異常的位置。
設定上面程式碼的執行引數:-Xms6m -Xmx6m
執行結果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(HashMap.java:704)
at java.util.HashMap.putVal(HashMap.java:663)
at java.util.HashMap.put(HashMap.java:612)
at java.util.HashSet.add(HashSet.java:220)
at com.oom.TestConstantPoolOOM.main(TestConstantPoolOOM.java:20)
可以看到,程式執行丟擲java.lang.OutOfMemoryError異常。
方法區的異常也是一種常見異常,一個類被垃圾回收期回收的條件是比較苛刻的,在經常執行時生成大量動態類的應用場景裡,就應該特別關注這些類的回收狀況,比如傳統專案中大量的jsp檔案,jsp會被編譯成Java類,容易丟擲方法區異常,在JDK8以後永久代不再存在,元空間的出現使得,正常建立物件的過程很難出現方法區的記憶體溢位,不過Hotspot也提供了一些引數設定保護元空間。
- XX:MaxMetaspaceSize:設定元空間最大值,預設是-1,即不限制,或者說只受限於本地記憶體大小。
- XX:MetaspaceSize:指定元空間的初始空間大小,以位元組為單位,達到該值就會觸發垃圾收集,收集器會對該值進行調整:如果釋放大量的空間,適當降低該值;如果釋放了很少的空間,在不超過-XX:MaxMetaspaceSize(如果設定了的話)的情況下,適當提高該值。
- XX:MinMetaspaceFreeRatio:作用是在垃圾收集之後控制最小的元空間剩餘容量的百分比,可減少因為元空間不足導致的垃圾收集的頻率。
- XX:Max-MetaspaceFreeRatio,用於控制最大的元空間剩餘容量的百分比。
直接記憶體
直接記憶體可以理解為堆外記憶體,通過引數-XX:MaxDirectMemorySize來設定,如果不設定預設與Java堆記憶體的最大值相同。NIO會使用直接記憶體,使用Unsafe類來模擬直接記憶體溢位的情況。
public class TestDirectMemoryOOM { 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); } } }
執行結果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.oom.TestDirectMemoryOOM.main(TestDirectMemoryOOM.java:24)
當直接記憶體溢位時,Dump檔案並沒有太多錯誤資訊,要考慮是否間接使用NIO了。
總結當Java虛擬機器丟擲OutOfMemoryError錯誤時,判斷是Java記憶體哪一塊區域丟擲的錯誤,定位堆,棧使用工具分析堆轉儲快照,判斷是記憶體洩漏(Memory Leak)還是記憶體溢位(Memory Overflow)當發生記憶體洩漏可以通過工具檢視GC Roots引用鏈,分析為什麼垃圾回收器無法回收,判定物件建立的位置,是否有可回收物件如果是記憶體溢位,根據硬體效能是否可以(使用-Xms和-Xmx)擴充套件堆記憶體大小,或者從程式碼角度去優化
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。