《深入理解Java虛擬機》
在Java虛擬機中,除了程序計數器外,其他幾個運行時區域都有發生OutOfMemoryError(OOM)異常的可能。關於虛擬機啓動參數,如果使用控制檯命令執行程序,直接跟在Java命令之後書寫就行;如果使用Eclipse IDE,可在如下圖配置:右鍵工程–Properties–選擇Run/Debug Settings–新建New–Ok
- JVM 堆溢出
Java 堆用於存儲對象的實例,只要不斷的創建對象,當對象數量達到最大堆容量限制後就會產生內存溢出異常。
設置Java堆大小爲10M,不可擴展(將堆的最小值-Xms和最大值-Xmx相等即可避免堆自動擴展),通過參數-XX:+HeapDumpOnOutOfMemoryError在出現內存溢出異常時Dump出當前內存堆轉儲快照以便事後分析。
代碼1 Java 堆內存溢出異常測試
/**
* VM Args: -verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
運行結果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid4756.hprof ...
Heap dump file created [13038231 bytes in 0.098 secs]
- Java棧和本地方法棧溢出
在HotSpot虛擬機中並不區分虛擬機棧和本地方法棧,因此,對於HotSpot來說,-Xoss(設置本地方法棧大小)雖存在但無效,棧容量只有-Xss參數設定。
對於Java棧和本地方法棧,Java虛擬機規範中描述了兩種異常:
- 若線程請求的棧深度大於虛擬機所允許的最大深度,拋出StackOverflowError異常
- 若虛擬機在擴展時無法申請足夠的內存空間,拋出OutOfMemoryError異常
(1)使用-Xss參數減少棧內存的容量,拋出StackOverflowError,異常出現時輸出棧深度相應縮小
(2)定義大量的本地變量,增大此方法幀中本地變量表的長度,拋出StackOverflowError,異常出現時輸出棧深度相應縮小
代碼2 Java 棧溢出異常測試
/**
* 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:995
Exception in thread "main" java.lang.StackOverflowError
at com.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
at com.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
... ...
在單個線程下,無論是由於棧幀太大還是虛擬機棧容量大小,但內存無法分配的時候,虛擬機拋出的都是StackOverflowError異常。
在多線程下,通過不斷的建立線程的方式可以產生內存溢出OutOfMemoryError異常
/**
* VM Args: -Xss2M
*/
public class JavaVMStackSOF {
public 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 {
JavaVMStackSOF oom = new JavaVMStackSOF();
oom.stackLeakByThread();
}
}
運行上述代碼有較大的風險,可能會導致操作系統的假死
運行結果:
Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread
- 方法區溢出
可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小
方法區用於存放Class的相關信息,類名、訪問修飾符、常量池、字段描述、方法描述等,基本思路是產生大量的類去填滿方法區,指到溢出。藉助CGLib直接操作字節碼生成大量的動態類。
代碼4 藉助CGLib使方法區內存溢出
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class JavaMethodAreaOOM {
static class OOMObject{
}
public static void main(String[] args){
ErrHandler handler = new ErrHandler();
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
}
運行結果:
java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid2824.hprof ...
Heap dump file created [3934632 bytes in 0.032 secs]
... ...
Exception: java.lang.OutOfMemoryError thrown from the
UncaughtExceptionHandlerin thread "main"
- 本機直接內存溢出
DirectMemory容量可通過-XX:MaxDirectMemorySize指定,如果不指定默認與Java堆最大值(-Xmx)一樣,下列代碼越過了DirectByteBuffer類,直接通過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法限制了只有引導類加載器纔會返回實例,也就是隻有rt.jar中的類才能使用Unsafe的功能)。
雖然使用DirectByteBuffer分配內存也會拋出異常,但它拋出異常時沒有真正向操作系統申請分配內存,而是通過計算得知內存無法分配,於是手動拋出異常,真正申請分配內存的方法是unsafe.allocateMemory().
代碼5 使用 unsafe 分配本機內存
/**
* sun.misc.Unsafe;
* 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 com.test.DirectMemoryOOM.main(DirectMemoryOOM.java:17)
由DirectMemory導致的內存溢出,如果發現OOM之後Dump文件很小,而程序中又直接或間接使用了NIO,可以考慮檢查是不是這方面的原因。