剖析Java OutOfMemoryError異常

剖析Java OutOfMemoryError異常

在JVM中,除了程式計數器外,虛擬機記憶體中的其他幾個運行時區域都有發生OutOfMemoryError異常的可能,本篇就來深入剖析一下各個區域出現OOM異常的情形,以及如何解決各個區域的OOM問題。

本篇主要包括如下內容:

  • Java堆溢出
  • 運行時常量池和方法區溢出
  • 本地記憶體溢出

Java堆溢出

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

堆溢出復現

要復現這種情況也很簡單:將Java堆的大小限制為固定值,且不可擴展(將堆的最小值-Xms參數與最大值-Xmx參數設置為一樣即可避免堆自動擴展);當使用一個 while(true) 循環來不斷創建對象就會發生 OutOfMemory,還可以使用 -XX:+HeapDumpOutofMemoryErorr 當發生 OOM 時會自動 dump 堆棧到文件中。

測試程式碼:

public static void main(String[] args) {          List<String> list = new ArrayList<>() ;          while (true){              list.add("1") ;          }      }

運行結果:

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:265)      at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)      at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)      at java.util.ArrayList.add(ArrayList.java:462)      at Main.main(Main.java:13)    Process finished with exit code 1

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space即是說發生了堆溢出。

原因

  1. 程式碼中可能存在大對象分配 ;
  2. 可能存在記憶體泄露,導致在多次GC之後,還是無法找到一塊足夠大的記憶體容納當前對象;
  3. 如果不是以上兩種情況,也就是說記憶體中的對象都必須存活,就應當檢查虛擬機的堆參數(-Xmx與-Xms),是否設置的堆記憶體空間太小,以及檢查程式碼中是否存在某些對象聲明周期過長、持有狀態時間過長的情況。

上面復現程式碼產生堆溢出的原因主要是第三點。

解決方法

  1. 檢查是否存在大對象的分配,最有可能的是大數組分配;
  2. 通過jmap命令,把堆記憶體dump下來,使用mat工具分析一下,檢查是否存在記憶體泄露的問題
  3. 如果沒有找到明顯的記憶體泄露,使用 -Xmx 加大堆記憶體;
  4. 還有一點容易被忽略,檢查是否有大量的自定義的 Finalizable 對象,也有可能是框架內部提供的,考慮其存在的必要性。

運行時常量池和方法區溢出

運行時常量池是方法區的一部分,我們先對運行時常量池溢出進行測試。

運行時常量池溢出復現

最典型的使用運行時常量池的方法是String的intern()方法,該方法是一個Native方法,它的作用是:如果字元串常量池中已經包含一個等於此String對象的字元串,則返回代表池中這個字元串的String對象;否則,將此String包含的字元串添加到常量池中,並且返回此String對象的引用。

在JDK1.6及以前的版本中,由於常量池分配在永久代中,可以通過-XX:PermSize和-XX:MaxPermSIze限制方法區大小,從而限制其中常量池的容量

測試程式碼:

public static void main(String[] args) {          List<String> list = new ArrayList<>();          int i = 0;          while (true) {              list.add(String.valueOf(i++).intern());          }      }

筆者所用為JDK1.8,已經去除了對這兩個JVM參數的支援,程式執行的結果如下:

Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0    Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0

暫不做深究。

方法區溢出復現

方法區用於存放class的相關資訊,包括類名、訪問修飾符、常量池、欄位描述、方法描述等。可以通過藉助CGLib直接操作位元組碼運行時生成大量的動態類,來填滿方法區。

PermSize 和 MaxPermSize 已經不能使用了,那在JDK1.8中怎麼設置方法區大小呢?

JDK 8 中將類資訊移到了本地堆記憶體(Native Heap)中,將原有的永久代移動到了本地堆中成為 MetaSpace ,如果不指定該區域的大小,JVM 將會動態的調整。

可以使用 -XX:MaxMetaspaceSize=10M 來限制最大元空間。這樣當不停的創建類時將會佔滿該區域並出現 OOM。

測試程式碼:

public static void main(String[] args) {          while (true){              Enhancer  enhancer = new Enhancer() ;              enhancer.setSuperclass(Main.class);              enhancer.setUseCache(false) ;              enhancer.setCallback(new MethodInterceptor() {                  @Override                  public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {                      return methodProxy.invoke(o,objects) ;                  }              });              enhancer.create() ;          }      }

設置好JVM參數後,執行上述程式碼,得到下面的額結果:

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace      at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:530)      at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)      at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:582)      at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)      at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)      at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:569)      at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:384)      at com.etekcity.cloud.Main.main(Main.java:27)    Process finished with exit code 1

這裡的 OOM 伴隨的是 Exception in thread "main" java.lang.OutOfMemoryError: Metaspace 也就是元空間溢出。

方法區溢出在應用中是比較常見的OOM異常,Spring、Hibernate等框架在對類進行增強時,都會使用到CGLib技術來增強類,增強的類越多,對方法區的容量要求就越大,就越可能出現方法區的OOM異常。

解決方法

因為該OOM原因比較簡單,解決方法有如下幾種:

  1. 檢查是否永久代空間或者元空間設置的過小;
  2. 檢查程式碼中是否存在大量的反射操作;
  3. dump之後通過mat檢查是否存在大量由於反射生成的代理類;
  4. 重啟JVM。

本機記憶體溢出

以上OOM異常都是出現於JVM內部,那麼如果是機器本身分給JVM的記憶體不夠導致溢出呢。

機器本身分給JVM的記憶體容量可以通過-XX:MaxDirectMemorySize指定,如果不指定,則默認與Java堆最大值(-Xmx指定一樣)。

可以通過反射獲取Unsafe實例進行記憶體分配,測試程式碼如下:

public static void main(String[] args) throws IllegalAccessException {          Field unsafeField = Unsafe.class.getDeclaredFields()[0];          unsafeField.setAccessible(true);          Unsafe unsafe = (Unsafe) unsafeField.get(null);          while (true) {              unsafe.allocateMemory(1024 * 1024);          }      }

運行結果如下:

Exception in thread "main" java.lang.OutOfMemoryError      at sun.misc.Unsafe.allocateMemory(Native Method)      at Main.main(Main.19)

有DirectMemory導致的記憶體溢出,在Heap Dump文件中不會看到明顯的異常,如果發現OOM之後的dump文件很小,可以考慮一下是否是這方面的原因。

Tags: