在VM運行時數據區域中,除了程序計數器,其餘在VM Spec中都描述了產生OutOfMemoryError(下稱OOM)的情形,那咱們就實戰模擬一下,經過幾段簡單的代碼,令對應的區域產生OOM異常以便加深認識,同時初步介紹一些與內存相關的虛擬機參數。下文的代碼都是基於Sun Hotspot虛擬機1.6版的實現,對於不一樣公司的不一樣版本的虛擬機,參數與程序運行結果可能結果會有所差異。 java
1.Java堆 多線程
Java堆存放的是對象實例,所以只要不斷創建對象,而且保證GC Roots到對象之間有可達路徑便可產生OOM異常。測試中限制Java堆大小爲20M,不可擴展,經過參數-XX:+HeapDumpOnOutOfMemoryError讓虛擬機在出現OOM異常的時候Dump出內存映像以便分析。(關於Dump映像文件分析方面的內容,可參見《JVM內存管理:深刻JVM內存異常分析與調優》。) 框架
Java堆OOM測試 ide
/** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { static class OOMObject { } public static void main( String[] args ) { java.util.List<OOMObject> list = new ArrayList<OOMObject>(); while( true ) { list.add( new OOMObject() ); } } }
運行結果: 測試
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid7372.hprof ... Heap dump file created [24724537 bytes in 0.699 secs]2.VM棧和本地方法棧
Hotspot虛擬機並不區分VM棧和本地方法棧,所以-Xoss參數其實是無效的,棧容量只由-Xss參數設定。關於VM棧和本地方法棧在VM Spec描述了兩種異常:StackOverflowError與OutOfMemoryError,當棧空間沒法繼續分配分配時,究竟是內存過小仍是棧太大其實某種意義上是對同一件事情的兩種描述而已,在筆者的實驗中,對於單線程應用嘗試下面3種方法均沒法讓虛擬機產生OOM,所有嘗試結果都是得到SOF異常。 spa
1.使用-Xss參數削減棧內存容量。結果:拋出SOF異常時的堆棧深度相應縮小。
2.定義大量的本地變量,增大此方法對應幀的長度。結果:拋出SOF異常時的堆棧深度相應縮小。
3.建立幾個定義不少本地變量的複雜對象,打開逃逸分析和標量替換選項,使得JIT編譯器容許對象拆分後在棧中分配。結果:實際效果同第二點 操作系統
清單2:VM棧和本地方法棧OOM測試(僅做爲第1點測試程序) 線程
/** * VM Args:-Xss128k * */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main( String[] args ) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch( Throwable e ) { System.out.println( "stack length:" + oom.stackLength ); throw e; } } }運行結果
stack length:3155 Exception in thread "main" java.lang.StackOverflowError at memoryManagement.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14) at memoryManagement.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14)若是在多線程環境下,不斷創建線程卻是能夠產生OOM異常,可是基本上這個異常和VM棧空間夠不夠沒有直接關係,甚至是給每一個線程的VM棧分配的內存越多反而越容易產生這個OOM異常。
緣由其實很好理解,操做系統分配給每一個進程的內存是有限制的,譬如32位Windows限制爲2G,Java堆和方法區的大小JVM有參數能夠限制最大值,那剩餘的內存爲2G(操做系統限制)-Xmx(最大堆)-MaxPermSize(最大方法區),程序計數器消耗內存很小,能夠忽略掉,那虛擬機進程自己耗費的內存不計算的話,剩下的內存就供每個線程的VM棧和本地方法棧瓜分了,那天然每一個線程中VM棧分配內存越多,就越容易把剩下的內存耗盡。 code
清單3:建立線程致使OOM異常 對象
/** * VM Args:-Xss2M (這時候不妨設大些) */ public class JavaVMStackOOM { private void dontStop() { while( true ) { } } public void stackLeakByThread() { while( true ) { Thread thread = new Thread( new Runnable() { @Override public void run() { dontStop(); } } ); thread.start(); } } public static void main( String[] args ) throws Throwable { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
運行結果
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
特別提示一下,若是讀者要運行上面這段代碼,記得要存盤當前工做,上述代碼執行時有很大令操做系統卡死的風險。
3.運行時常量池
要在常量池裏添加內容,最簡單的就是使用String.intern()這個Native方法。因爲常量池分配在方法區內,咱們只須要經過-XX:PermSize和-XX:MaxPermSize限制方法區大小便可限制常量池容量。實現代碼以下
清單4:運行時常量池致使的OOM異常
/** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * */ public class RuntimeConstantPoolOOM { public static void main( String[] args ) { // 使用List保持着常量池引用,壓制Full GC回收常量池行爲 List<String> list = new ArrayList<String>(); // 10M的PermSize在integer範圍內足夠產生OOM了 int i = 0; while( true ) { list.add( String.valueOf( i++ ).intern() ); } } }運行結果
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(Unknown Source)
4.方法區
方法區用於存放Class相關信息,因此這個區域的測試咱們藉助CGLib直接操做字節碼動態生成大量的Class,值得注意的是,這裏咱們這個例子中模擬的場景其實常常會在實際應用中出現:當前不少主流框架,如Spring、Hibernate對類進行加強時,都會使用到CGLib這類字節碼技術,當加強的類越多,就須要越大的方法區用於保證動態生成的Class能夠加載入內存。
清單5:藉助CGLib使得方法區出現OOM異常
/** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M * */ public class JavaMethodAreamOOM { public static void main( String[] args ) { while( true ) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass( OOMObject.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 OOMObject { } }
運行結果
Caused by: java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616) ... 8 more
5. 本機直接內存
DirectMemory容量可經過-XX:MaxDirectMemorySize指定,不指定的話默認與Java堆(-Xmx指定)同樣,下文代碼越過了DirectByteBuffer,直接經過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法限制了只有引導類加載器纔會返回實例,也就是基本上只有rt.jar裏面的類的才能使用),由於DirectByteBuffer也會拋OOM異常,但拋出異常時實際上並無真正向操做系統申請分配內存,而是經過計算得知沒法分配既會拋出,真正申請分配的方法是unsafe.allocateMemory()
/** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * */ 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 ); } } }運行結果
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)