Java程序員必備:常見OOM異常分析

前言

放假這幾天,溫習了深入理解Java虛擬機的第二章, 整理了JVM發生OOM異常的幾種情況,並分析原因以及解決方案,希望對大家有幫助。

Java 堆溢出

Java堆用於存儲對象實例,只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生內存溢出異常。

Java 堆溢出原因

  • 無法在 Java 堆中分配對象

  • 應用程序保存了無法被GC回收的對象。

  • 應用程序過度使用 finalizer。

Java 堆溢出排查解決思路

1.查找關鍵報錯信息,如

  1. java.lang.OutOfMemoryError: Java heap space

2.使用內存映像分析工具(如Eclipsc Memory Analyzer或者Jprofiler)對Dump出來的堆儲存快照進行分析,分析清楚是內存泄漏還是內存溢出。

3.如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈,修復應用程序中的內存泄漏。

4.如果不存在泄漏,先檢查代碼是否有死循環,遞歸等,再考慮用 -Xmx 增加堆大小。

demo代碼

  1. package oom;

  2. import java.util.ArrayList;

  3. import java.util.List;

  4. /**

  5. * JVM配置參數

  6. * -Xms20m JVM初始分配的內存20m

  7. * -Xmx20m JVM最大可用內存爲20m

  8. * -XX:+HeapDumpOnOutOfMemoryError 當JVM發生OOM時,自動生成DUMP文件

  9. * -XX:HeapDumpPath=/Users/weihuaxiao/Desktop/dump/ 生成DUMP文件的路徑

  10. */

  11. public class HeapOOM {

  12. static class OOMObject {

  13. }

  14. public static void main(String[] args) {

  15. List<OOMObject> list = new ArrayList<OOMObject>();

  16. //在堆中無限創建對象

  17. while (true) {

  18. list.add(new OOMObject());

  19. }

  20. }

  21. }

運行結果

按照前面的排查解決方案,我們來一波分析。

1.查找報錯關鍵信息

  1. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

2. 使用內存映像分析工具Jprofiler分析產生的堆儲存快照

由圖可得,OOMObject這個類創建了810326個實例,是屬於內存溢出,這時候先定位到對應代碼,發現死循環導致的,修復即可。

棧溢出

關於虛擬機棧和本地方法棧,在Java虛擬機規範中描述了兩種異常:

  • 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError 異常;

  • 如果虛擬機棧可以動態擴展,當擴展時無法申請到足夠的內存時會拋出 OutOfMemoryError 異常。

棧溢出原因

  • 在單個線程下,棧幀太大,或者虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出StackOverflowError 異常。

  • 不斷地建立線程的方式會導致內存溢出。

棧溢出排查解決思路

  1. 查找關鍵報錯信息,確定是StackOverflowError還是OutOfMemoryError

  2. 如果是StackOverflowError,檢查代碼是否遞歸調用方法等

  3. 如果是OutOfMemoryError,檢查是否有死循環創建線程等,通過-Xss降低的每個線程棧大小的容量

demo代碼

  1. package oom;

  2. /**

  3. * -Xss2M

  4. */

  5. public class JavaVMStackOOM {

  6. private void dontStop(){

  7. while(true){

  8. }

  9. }

  10. public void stackLeakByThread(){

  11. while(true){

  12. Thread thread = new Thread(new Runnable(){

  13. public void run() {

  14. dontStop();

  15. }

  16. });

  17. thread.start();}

  18. }

  19. public static void main(String[] args) {

  20. JavaVMStackOOM oom = new JavaVMStackOOM();

  21. oom.stackLeakByThread();

  22. }

  23. }

運行結果

1.查找報錯關鍵信息

  1. Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

2.確定是創建線程導致的棧溢出OOM

  1. Thread thread = new Thread(new Runnable(){

  2. public void run() {

  3. dontStop();

  4. }

  5. });

3.排查代碼,確定是否顯示使用死循環創建線程,或者隱式調用第三方接口創建線程(之前公司,調用騰訊雲第三方接口,上傳圖片,遇到這個問題)

方法區溢出

方法區,(又叫永久代,JDK8後,元空間替換了永久代),用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。運行時產生大量的類,會填滿方法區,造成溢出。

方法區溢出原因

  • 使用CGLib生成了大量的代理類,導致方法區被撐爆

  • 在Java7之前,頻繁的錯誤使用String.intern方法

  • 大量jsp和動態產生jsp

  • 應用長時間運行,沒有重啓

方法區溢出排查解決思路

  • 檢查是否永久代空間設置得過小

  • 檢查代碼是否頻繁錯誤得使用String.intern方法

  • 檢查是否跟jsp有關。

  • 檢查是否使用CGLib生成了大量的代理類

  • 重啓大法,重啓JVM

demo代碼

  1. package oom;

  2. import org.springframework.cglib.proxy.Enhancer;

  3. import org.springframework.cglib.proxy.MethodInterceptor;

  4. import org.springframework.cglib.proxy.MethodProxy;

  5. import java.lang.reflect.Method;

  6. /**

  7. * jdk8以上的話,

  8. * 虛擬機參數:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

  9. */

  10. public class JavaMethodAreaOOM {

  11. public static void main(String[] args) {

  12. while (true) {

  13. Enhancer enhancer = new Enhancer();

  14. enhancer.setSuperclass(OOMObject.class);

  15. enhancer.setUseCache(false);

  16. enhancer.setCallback(new MethodInterceptor() {

  17. public Object intercept(Object obj, Method method,

  18. Object[] args, MethodProxy proxy) throws Throwable {

  19. return proxy.invokeSuper(obj, args);

  20. }

  21. });

  22. enhancer.create();

  23. }

  24. }

  25. static class OOMObject {

  26. }

  27. }

運行結果

1.查找報錯關鍵信息

  1. Caused by: java.lang.OutOfMemoryError: Metaspace

2.檢查JVM元空間設置參數是否過小

  1. -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

3. 檢查對應代碼,是否使用CGLib生成了大量的代理類

  1. while (true) {

  2. ...

  3. enhancer.setCallback(new MethodInterceptor() {

  4. public Object intercept(Object obj, Method method,

  5. Object[] args, MethodProxy proxy) throws Throwable {

  6. return proxy.invokeSuper(obj, args);

  7. }

  8. });

  9. enhancer.create();

  10. }

本機直接內存溢出

直接內存並不是虛擬機運行時數據區的一部分,也不是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代碼

  1. package oom;

  2. import java.nio.ByteBuffer;

  3. import java.util.concurrent.TimeUnit;

  4. /**

  5. * -Xmx256m -XX:MaxDirectMemorySize=100M

  6. */

  7. public class DirectByteBufferTest {

  8. public static void main(String[] args) throws InterruptedException{

  9. //分配128MB直接內存

  10. ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);

  11. TimeUnit.SECONDS.sleep(10);

  12. System.out.println("ok");

  13. }

  14. }

運行結果

ByteBuffer分配128MB直接內存,而JVM參數-XX:MaxDirectMemorySize=100M指定最大是100M,因此發生直接內存溢出。

  1. ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);

GC overhead limit exceeded

  • 這個是JDK6新加的錯誤類型,一般都是堆太小導致的。

  • Sun 官方對此的定義:超過98%的時間用來做GC並且回收了不到2%的堆內存時會拋出此異常。

解決方案

  • 檢查項目中是否有大量的死循環或有使用大內存的代碼,優化代碼。

  • 檢查JVM參數-Xmx -Xms是否合理

  • dump內存,檢查是否存在內存泄露,如果沒有,加大內存。

demo代碼

  1. package oom;

  2. import java.util.concurrent.ExecutorService;

  3. import java.util.concurrent.Executors;

  4. /**

  5. * JVm參數 -Xmx8m -Xms8m

  6. */

  7. public class GCoverheadTest {

  8. public static void main(String[] args) {

  9. ExecutorService executor = Executors.newFixedThreadPool(10);

  10. for (int i = 0; i < Integer.MAX_VALUE; i++) {

  11. executor.execute(() -> {

  12. try {

  13. Thread.sleep(10000);

  14. } catch (InterruptedException e) {

  15. //do nothing

  16. }

  17. });

  18. }

  19. }

  20. }

運行結果

實例代碼使用了newFixedThreadPool線程池,它使用了無界隊列,無限循環執行任務,會導致內存飆升。因爲設置了堆比較小,所以出現此類型OOM。

總結

本文介紹了以下幾種常見OOM異常

  1. java.lang.OutOfMemoryError: Java heap space

  2. java.lang.OutOfMemoryError: unable to create new native thread

  3. java.lang.OutOfMemoryError: Metaspace

  4. java.lang.OutOfMemoryError: Direct buffer memory

  5. java.lang.OutOfMemoryError: GC overhead limit exceeded

希望大家遇到OOM異常時,對症下藥,順利解決問題。同時,如果有哪裏寫得不對,歡迎指出,感激不盡。

參考與感謝

  • JVM系列之實戰內存溢出異常

  • JVM 發生 OOM 的 8 種原因、及解決辦法

  • NIO-直接內存

  • 《深入理解Java虛擬機》

個人公衆號

  • 如果你是個愛學習的好孩子,可以關注我公衆號,一起學習討論。

  • 如果你覺得本文有哪些不正確的地方,可以評論,也可以關注我公衆號,私聊我,大家一起學習進步哈。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章