Java8虛擬機(JVM)內存溢出實戰

  • 2019 年 10 月 3 日
  • 筆記

前言

相信很多JAVA中高級的同學在面試的時候會經常碰到一個面試題
你是如何在工作中對JVM調優和排查定位問題的

事實上,如果用戶量不大的情況下,在你的代碼還算正常的情況下,在工作中除非真正碰到與JVM相關的問題是少之又少,就算碰到了也是由公司的一些大牛去排查解決,那麼我們又如何積累這方面的經驗呢?下面由沖鍋帶大家一起來實踐JVM的調優吧

注意我們平常所說的JVM調優一般指Java堆,Java虛擬機棧參數調優

Java堆溢出

先來一段代碼示例,注意筆者用的是IDEA工具,需要配置一下VM options 為-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError,如果不清楚的百度一下如何配置idea的JVM運行參數

package com.example.demo.jvm;    import java.util.ArrayList;  import java.util.List;    /**   * @Author: Wang Chong   * @Date: 2019/9/22 9:37   * @Version: V1.0   */  public class HeapOutMemoryTest {      static class ChongGuo {        }      public static void main(String[] args) {          List<ChongGuo> chongGuos = new ArrayList<>();          while (true) {              chongGuos.add(new ChongGuo());          }      }  }  

運行結果如下:

java.lang.OutOfMemoryError: Java heap space  Dumping heap to java_pid9352.hprof ...  Heap dump file created [28701160 bytes in 0.122 secs]  Exception in thread "main" java.lang.OutOfMemoryError: Java heap space      at java.util.Arrays.copyOf(Arrays.java:3210)      at java.util.Arrays.copyOf(Arrays.java:3181)      at java.util.ArrayList.grow(ArrayList.java:261)      at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)      at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)      at java.util.ArrayList.add(ArrayList.java:458)      at com.example.demo.jvm.HeapOutMemoryTest.main(HeapOutMemoryTest.java:18)  Disconnected from the target VM, address: '127.0.0.1:54599', transport: 'socket'

可以看到控制台出現java.lang.OutOfMemoryError: Java heap space的錯誤,這是為什麼呢,首先先解釋一下上面的運行參數

-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

  • -Xms20m:設置JVM最小內存為20m。此值可以設置與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配內存
  • -Xmx20m:設置JVM最大可用內存20M
  • -XX:+HeapDumpOnOutOfMemoryError 表示當JVM發生OOM時,自動生成DUMP文件

下面我們分析一下出錯的原因,用JProfiler分析一下,打開剛才生成的名為java_pid9352.hprof的dump文件。可以看到根據(InstanceXcount和Size)基本可以確定哪個類的對象出現問題,在上面示例中,可以是ChongGuo這個實例生在數量的大小已經超過12M,但沒有超過20M,那麼新問題又來了?沒到20M為啥會報堆內存溢出呢

答案就是JDK8中堆內存中還包括Metaspace,即元內存空間,在元空間出現前JDK1.7之前在JDK7以及其前期的JDK版本號中。堆內存通常被分為三塊區域Nursery內存(young generation)、長時內存(old generation)、永久內存(Permanent Generation for VM Matedata),如下圖

當中最上一層是年輕代,一個對象被創建以後首先被放到年輕代中的Eden內存中,假設存活期超兩個Survivor之後就會被轉移到長時內存中(Old Generation)中永久內存中存放着對象的方法、變量等元數據信息。通過假設永久內存不夠。我們就會得到例如以下錯誤:java.lang.OutOfMemoryError: PermGen
而在JDK8中情況發生了明顯的變化,就是普通情況下你都不會得到這個錯誤,原因
在於JDK8中把存放元數據中的永久內存從堆內存中移到了本地內存(native memory)
中,JDK8中JVM堆內存結構就變成了例如以下:

如果我啟動VM參數加上:-XX:MaxMetaspaceSize=1m,重新運行一下上面的程序,

Connected to the target VM, address: '127.0.0.1:56433', transport: 'socket'  java.lang.OutOfMemoryError: Metaspace  Dumping heap to java_pid9232.hprof ...  Heap dump file created [1604635 bytes in 0.024 secs]  FATAL ERROR in native method: processing of -javaagent failed  Exception in thread "main" Disconnected from the target VM, address: '127.0.0.1:56433', transport: 'socket'    Process finished with exit code 1

可以發現報錯信息變成了java.lang.OutOfMemoryError: Metaspace,說明元空間不夠,我改成到大概4m左右才能滿足啟動條件。

虛擬機棧和本地方法棧棧溢出

在Java虛擬機規範中描述了兩種異常:

  • 如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常
  • 如果虛擬機在擴展棧無法申請到足夠的內存空間,則拋出OutOfMemoryError異常

    StackOverflowError比較好測試,測試代碼如下:

package com.example.demo.jvm;    /**   * @Author: Wang Chong   * @Date: 2019/9/22 19:09   * @Version: V1.0   */  public class StackOverflowTest {        /**       * 棧大小       */      private int stackLength = 1;        /**       * 遞歸壓棧       */      public void stackLeak() {          stackLength++;          stackLeak();      }        public static void main(String[] args) {          StackOverflowTest stackOverflowTest = new StackOverflowTest();          try {              stackOverflowTest.stackLeak();          } catch (Throwable e) {              System.out.println("stack length is :" + stackOverflowTest.stackLength);              throw e;          }        }    }  

運行結果如下:

Exception in thread "main" stack length is :20739  java.lang.StackOverflowError      at com.example.demo.jvm.StackOverflowTest.stackLeak(StackOverflowTest.java:20)      at com.example.demo.jvm.StackOverflowTest.stackLeak(StackOverflowTest.java:20)

在VM參數-Xss參數未設置的情況下,該線程的內存支持的棧深度為20739,該測試結果與機器的內存大小有關,不過上面的第二點如何測試呢?正常來說如果是單線程,則難以測試內存泄露的情況,那麼多線程呢?我們看一下以下測試代碼:

package com.example.demo.jvm;    /**   * @Author: Wang Chong   * @Date: 2019/9/22 19:09   * @Version: V1.0   */  public class StackOOMTest implements Runnable{        /**       * 棧大小       */      private int stackLength = 1;        /**       * 遞歸壓棧       */      public void stackLeak() {          stackLength++;          stackLeak();      }        public static void main(String[] args) {         while (true){             StackOOMTest stackOverflowTest = new StackOOMTest();             new Thread(stackOverflowTest).start();         }        }        @Override      public void run() {         stackLeak();      }  }  

如果系統不假死的情況下,會出現Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread

運行時常量池溢出

  • 字符型常量池溢出,在JAVA8中也是堆溢出,測試代碼如下:
package com.example.demo.jvm;    import java.util.ArrayList;  import java.util.List;    /**   * @Author: Wang Chong   * @Date: 2019/9/22 19:44   * @Version: V1.0   */  public class RuntimePoolOOMTest {        public static void main(String[] args) {          List<String> list = new ArrayList<>();          int i = 0;          while (true) {              list.add(String.valueOf(i).intern());          }      }  }  

結果如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space      at java.util.Arrays.copyOf(Arrays.java:3210)      at java.util.Arrays.copyOf(Arrays.java:3181)      at java.util.ArrayList.grow(ArrayList.java:261)      at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)      at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)      at java.util.ArrayList.add(ArrayList.java:458)      at com.example.demo.jvm.RuntimePoolOOMTest.main(RuntimePoolOOMTest.java:17)  Disconnected from the target VM, address: '127.0.0.1:50253', transport: 'socket'

證明字符常量池已經在Java8中是在堆中分配的。

方法區溢出

在Java7之前,方法區位於永久代(PermGen),永久代和堆相互隔離,永久代的大小在啟動JVM時可以設置一個固定值,不可變;Java8仍然保留方法區的概念,只不過實現方式不同。取消永久代,方法存放於元空間(Metaspace),元空間仍然與堆不相連,但與堆共享物理內存,邏輯上可認為在堆中
測試代碼如下,為快速看出結果,請加入VM參數-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=10m:

package com.example.demo.jvm;    import org.springframework.cglib.proxy.Enhancer;  import org.springframework.cglib.proxy.MethodInterceptor;    /**   * @Author: Wang Chong   * @Date: 2019/9/22 19:56   * @Version: V1.0   */  public class MethodAreaOOMTest {      public static void main(String[] args) {          while (true) {              Enhancer enhancer = new Enhancer();              enhancer.setSuperclass(OOMObject.class);              enhancer.setUseCache(false);              enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o,                      objects));              enhancer.create();          }      }        static class OOMObject {        }  }

運行結果如下:

java.lang.OutOfMemoryError: Metaspace  Dumping heap to java_pid8816.hprof ...  Heap dump file created [6445908 bytes in 0.039 secs]  Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null      at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)      at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:492)      at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)      at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)      at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)      at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:305)      at com.example.demo.jvm.MethodAreaOOMTest.main(MethodAreaOOMTest.java:19)  Caused by: java.lang.reflect.InvocationTargetException      at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)      at java.lang.reflect.Method.invoke(Method.java:498)      at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)      at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)      ... 6 more  Caused by: java.lang.OutOfMemoryError: Metaspace      at java.lang.ClassLoader.defineClass1(Native Method)      at java.lang.ClassLoader.defineClass(ClassLoader.java:763)      ... 11 more    Process finished with exit code 1

元空間內存報錯,證明方法區的溢出與元空間相關。

總結如下:

  • 正常JVM調優都是針對堆內存和棧內存、元空間的參數做相應的改變
  • 元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:
  • -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
  • -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
  • 字符串池常量池在每個VM中只有一份,存放的是字符串常量的引用值,存放在堆中

有更多的文章,請關注查看,更有面試寶典相送
image