1. 程式人生 > >JVM中OutOfMemoryError異常案例一方法區和直接記憶體

JVM中OutOfMemoryError異常案例一方法區和直接記憶體

記憶體溢位之方法區和直接記憶體的介紹

通過實驗來介紹方法區直接記憶體區 的OOM

一、方法區

:在JDK1.6 及以前版本中,由於常量池分配在永久代內,可以通過-XX:PermSize-XX:MaxPermSize 限制方法區的大小,從而間接限制其中常量池的容量。 注意JDK1.8已經使用元空間 來代替永久區,所以在1.8中,這兩個引數將被忽略,而改使用-XX:MetaspaceSize -XX:MaxMetaspaceSize 來控制元空間

案例一

案例描述: 執行時常量池導致的記憶體溢位異常

引數設定:

下面這個執行時常量池導致的記憶體溢位異常的案例 是JDK1.6

  -XX:PermSize=10M -XX:MaxPermSize=10M  

原始碼:

import java.util.ArrayList;
import java.util.List;
/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 * JDK1.6
 */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        // 使用List保持著常量池引用,避免Full GC回收常量池行為
        List<String> list = new
ArrayList<String>(); // 10MB的PermSize在integer範圍內足夠產生OOM了 int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } }

執行結果:

Exception in thread “main” java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)

String.intern()是一個Native方法,它的作用是:如果字串 常量池這中已經包含此String物件的字串,則返回代表池中這個字串的String物件;否則,將此String物件包含的字串新增到常量池中,並且返回此String物件的引用。

注意: 使用JDK1.7 迴圈則將一直進行,得不到結果。 原因?

: JVM初始化也需要佔用一定的開銷。記憶體太小,在進行vm初始化時觸發GC

案例二:

案例描述: String.intern() 返回引用測試:

原始碼:

public class RuntimeConstantPoolOOM2 {

    public static void main(String[] args) {
            String str1 = new StringBuilder("中國").append("釣魚島").toString();
            System.out.println(str1.intern() == str1);

            String str2 = new StringBuilder("ja").append("va").toString();
            System.out.println(str2.intern() == str2);
        }
}

結果:

  • JDK1.6 : false false
  • JDK1.7 : true false

結果分析:

  • JDK1.6 中,intern() 方法會把首次遇到的字串例項複製到永久代中,返回的也是永久代中這個字串例項的引用,而由StringBuilder建立的字串例項在java堆上,所以必然不是同一個引用。將返回false.
  • JDK1.7 中 的 intern() 實現不會在複製例項,只是在常量池中記錄首次出現的例項引用。因此intern()和 由StringBuilder 建立的那個字串例項是同一個。
  • 而str2 比較返回false是因為 java 這個字串在執行 StringBuilder.toString()之前出現過,字串常量池已經有它的引用了。不符合“首次出現” 的原則。而“中國釣魚島”字串則是首次出現。因此返回true.

案例三

如何讓方法區丟擲異常。下面的思路時執行時產生大量的類去填充方法區,導致溢位。

注: 當前很多框架,對類進行增強時,都會使用CGLib這個類位元組碼技術。

原始碼:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * JDK1.7以下執行,
 */
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 {

    }
}

執行結果:

Caused by: java.lang.OutOfMemoryError: PermGen space
...

---

JDK1.8

在JDK1.8中,需要將引數修改

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

引數注意大小寫;和-XX:PermSize=10M -XX:MaxPermSize=10M 在1.8之前是相同效果

執行結果:

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)
    at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
    at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
    at JavaMethodAreaOOM.main(JavaMethodAreaOOM.java:23)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

結果分析:

Metaspace(元空間)記憶體溢位了。

二、直接記憶體

引數:

-XX:MaxDirectMemorySize 指定直接記憶體的大小,如果不指定,則預設和java堆最大值(-Xmx指定)一樣。

原始碼:

import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
 * 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 outofmemoryerror.DirectMemoryOOM.main(DirectMemoryOOM.java:19)

後記

(一)疑問:

  1. JDK1.8 元空間的大小限制是與什麼有關? 是與實體記憶體?
  2. 為什麼JDK1.7 執行 RuntimeConstantPoolOOM 這個類會始終迴圈而不結束(與intern在不同版本中的實現有關?)
  3. -XX:MaxDirectMemorySize 指定直接記憶體的大小,如果不指定,則預設和java堆最大值(-Xmx指定)一樣。正確嗎?

(二)重點:

  1. 理解概念,兩種記憶體溢位是怎麼回事,掌握概念。不同版本執行結果可能不同,但是原理概念是相通。
  2. 理解intern 這個方法

參考

  • 《深入理解java虛擬機器》

注意: 不同版本的JDK設定相同的虛擬機器引數結果可能不一樣。二八原則,不需要太過於專注細節。