實戰OutOfMemoryError
在VM運行時數據區域中,除了程序計數器,其他在VM Spec中都描述了產生OutOfMemoryError(下稱OOM)的情形,那我們就實戰模擬一下,通過幾段簡單的代碼,令對應的區域產生OOM異常以便加深認識,同時初步介紹一些與內存相關的虛擬機參數。下文的代碼都是基于Sun Hotspot虛擬機1.6版的實現,對于不同公司的不同版本的虛擬機,參數與程序運行結果可能結果會有所差別。
1.Java堆
Java堆存放的是對象實例,因此只要不斷建立對象,并且保證GC Roots到對象之間有可達路徑即可產生OOM異常。測試中限制Java堆大小為20M,不可擴展,通過參數-XX:+HeapDumpOnOutOfMemoryError讓虛擬機在出現OOM異常的時候Dump出內存映像以便分析。(關于Dump映像文件分析方面的內容,可參見《JVM內存管理:深入JVM內存異常分析與調優》。)
Java堆OOM測試
/** * 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異常。
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棧分配內存越多,就越容易把剩下的內存耗盡。
清單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)