前言
放假這幾天,溫習了深入理解Java虛擬機的第二章, 整理了JVM發生OOM異常的幾種情況,並分析原因以及解決方案,希望對大家有幫助。
Java 堆溢出
Java堆用於存儲對象實例,只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生內存溢出異常。
Java 堆溢出原因
無法在 Java 堆中分配對象
應用程序保存了無法被GC回收的對象。
應用程序過度使用 finalizer。
Java 堆溢出排查解決思路
1.查找關鍵報錯信息,如
java.lang.OutOfMemoryError: Java heap space
2.使用內存映像分析工具(如Eclipsc Memory Analyzer或者Jprofiler)對Dump出來的堆儲存快照進行分析,分析清楚是內存泄漏還是內存溢出。
3.如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈,修復應用程序中的內存泄漏。
4.如果不存在泄漏,先檢查代碼是否有死循環,遞歸等,再考慮用 -Xmx 增加堆大小。
demo代碼
package oom;
import java.util.ArrayList;
import java.util.List;
/**
* JVM配置參數
* -Xms20m JVM初始分配的內存20m
* -Xmx20m JVM最大可用內存爲20m
* -XX:+HeapDumpOnOutOfMemoryError 當JVM發生OOM時,自動生成DUMP文件
* -XX:HeapDumpPath=/Users/weihuaxiao/Desktop/dump/ 生成DUMP文件的路徑
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
//在堆中無限創建對象
while (true) {
list.add(new OOMObject());
}
}
}
運行結果
按照前面的排查解決方案,我們來一波分析。
1.查找報錯關鍵信息
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
2. 使用內存映像分析工具Jprofiler分析產生的堆儲存快照
由圖可得,OOMObject這個類創建了810326個實例,是屬於內存溢出,這時候先定位到對應代碼,發現死循環導致的,修復即可。
棧溢出
關於虛擬機棧和本地方法棧,在Java虛擬機規範中描述了兩種異常:
如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError 異常;
如果虛擬機棧可以動態擴展,當擴展時無法申請到足夠的內存時會拋出 OutOfMemoryError 異常。
棧溢出原因
在單個線程下,棧幀太大,或者虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出StackOverflowError 異常。
不斷地建立線程的方式會導致內存溢出。
棧溢出排查解決思路
查找關鍵報錯信息,確定是StackOverflowError還是OutOfMemoryError
如果是StackOverflowError,檢查代碼是否遞歸調用方法等
如果是OutOfMemoryError,檢查是否有死循環創建線程等,通過-Xss降低的每個線程棧大小的容量
demo代碼
package oom;
/**
* -Xss2M
*/
public class JavaVMStackOOM {
private void dontStop(){
while(true){
}
}
public void stackLeakByThread(){
while(true){
Thread thread = new Thread(new Runnable(){
public void run() {
dontStop();
}
});
thread.start();}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
運行結果
1.查找報錯關鍵信息
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
2.確定是創建線程導致的棧溢出OOM
Thread thread = new Thread(new Runnable(){
public void run() {
dontStop();
}
});
3.排查代碼,確定是否顯示使用死循環創建線程,或者隱式調用第三方接口創建線程(之前公司,調用騰訊雲第三方接口,上傳圖片,遇到這個問題)
方法區溢出
方法區,(又叫永久代,JDK8後,元空間替換了永久代),用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。運行時產生大量的類,會填滿方法區,造成溢出。
方法區溢出原因
使用CGLib生成了大量的代理類,導致方法區被撐爆
在Java7之前,頻繁的錯誤使用String.intern方法
大量jsp和動態產生jsp
應用長時間運行,沒有重啓
方法區溢出排查解決思路
檢查是否永久代空間設置得過小
檢查代碼是否頻繁錯誤得使用String.intern方法
檢查是否跟jsp有關。
檢查是否使用CGLib生成了大量的代理類
重啓大法,重啓JVM
demo代碼
package oom;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* jdk8以上的話,
* 虛擬機參數:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
*/
public class JavaMethodAreaOOM {
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 {
}
}
運行結果
1.查找報錯關鍵信息
Caused by: java.lang.OutOfMemoryError: Metaspace
2.檢查JVM元空間設置參數是否過小
-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
3. 檢查對應代碼,是否使用CGLib生成了大量的代理類
while (true) {
...
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
本機直接內存溢出
直接內存並不是虛擬機運行時數據區的一部分,也不是Java 虛擬機規範中定義的內存區域。但是,這部分內存也被頻繁地使用,而且也可能導致OOM。
在JDK1.4 中新加入了NIO(New Input/Output)類,它可以使用 native 函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的 DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在 Java 堆和 Native 堆中來回複製數據。
直接內存溢出原因
本機直接內存的分配雖然不會受到Java 堆大小的限制,但是受到本機總內存大小限制。
直接內存由 -XX:MaxDirectMemorySize 指定,如果不指定,則默認與Java堆最大值(-Xmx指定)一樣。
NIO程序中,使用ByteBuffer.allocteDirect(capability)分配的是直接內存,可能導致直接內存溢出。
直接內存溢出
檢查代碼是否恰當
檢查JVM參數-Xmx,-XX:MaxDirectMemorySize 是否合理。
demo代碼
package oom;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
/**
* -Xmx256m -XX:MaxDirectMemorySize=100M
*/
public class DirectByteBufferTest {
public static void main(String[] args) throws InterruptedException{
//分配128MB直接內存
ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
TimeUnit.SECONDS.sleep(10);
System.out.println("ok");
}
}
運行結果
ByteBuffer分配128MB直接內存,而JVM參數-XX:MaxDirectMemorySize=100M指定最大是100M,因此發生直接內存溢出。
ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
GC overhead limit exceeded
這個是JDK6新加的錯誤類型,一般都是堆太小導致的。
Sun 官方對此的定義:超過98%的時間用來做GC並且回收了不到2%的堆內存時會拋出此異常。
解決方案
檢查項目中是否有大量的死循環或有使用大內存的代碼,優化代碼。
檢查JVM參數-Xmx -Xms是否合理
dump內存,檢查是否存在內存泄露,如果沒有,加大內存。
demo代碼
package oom;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* JVm參數 -Xmx8m -Xms8m
*/
public class GCoverheadTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
}
}
}
運行結果
實例代碼使用了newFixedThreadPool線程池,它使用了無界隊列,無限循環執行任務,會導致內存飆升。因爲設置了堆比較小,所以出現此類型OOM。
總結
本文介紹了以下幾種常見OOM異常
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: unable to create new native thread
java.lang.OutOfMemoryError: Metaspace
java.lang.OutOfMemoryError: Direct buffer memory
java.lang.OutOfMemoryError: GC overhead limit exceeded
希望大家遇到OOM異常時,對症下藥,順利解決問題。同時,如果有哪裏寫得不對,歡迎指出,感激不盡。
參考與感謝
JVM系列之實戰內存溢出異常
JVM 發生 OOM 的 8 種原因、及解決辦法
NIO-直接內存
《深入理解Java虛擬機》
個人公衆號
如果你是個愛學習的好孩子,可以關注我公衆號,一起學習討論。
如果你覺得本文有哪些不正確的地方,可以評論,也可以關注我公衆號,私聊我,大家一起學習進步哈。