转载

Java内存溢出异常(下)

此篇是上一篇文章 Java内存溢出异常(上) 的续篇,没有看过的同学,可以先看一下上篇。本篇文章将介绍剩余的两个溢出异常:方法区和运行时常量池溢出。

方法区和运行时常量池溢出

这部分为什么会放在一起呢?在Java 内存区域与内存溢出异常 这篇文章中我们说过,运行时常量池实际上属于方法区的一部分,所以就放在一起讨论。

常量池溢出

在讨论常量池的溢出之前,先说明一下String.intern()方法,该方法会检查字符串常量池中是否已经包含了一个等于此String对象的字符串,如果已经包含了,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。看过fastjson源代码的同学应该知道,该方法在fastjson这个json解析库中大量的出现,以提高json的解析速度。

在JDK 1.6之前的版本中,常量池是分配在永久代的,可以通过-XX:PermSize和-XX:MaxPermSize参数来设置大小,从而间接限制其中常量池的容量。

通过以上条件,我们可以轻易的复现这个异常,代码如下:

public class RuntimeConstantPoolOOM {

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

如果你的JDK环境是1.6版本之前的,你会得到如下运行结果:

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

如果你是JDK 1.7+,那么这段代码会无限的运行下去。因为在JDK 1.7之后对String.intern()方法进行了修改。

继续看下面这段经典的代码:

public class RuntimeConstantPoolOOM {

    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);
    }
}

这段代码在JDK 1.6中运行,会得到两个false,而在JDK 1.7中运行,会得到一个true和一个false。

出现差异的原因是因为,在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到常量池中,返回的也是常量池中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。而JDK 1.7中的intern()实训不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过了,字符串中常量池中已经有它的引用了,不符合“首次”出现的原则。

这就可以解释,为什么在JDK 1.7+版本之后不能用String.intern()方法使常量池溢出的原因了,intern()不会像JDK 1.6之前的版本一样无限复制实例到常量池中了。

方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。这部分的测试可以通过生成大量的类去填满方法区,直到溢出,可以借助CGLib直接操作字节码运行时生成大量的动态类,代码如下:

public class JavaMethodAreaOOM {

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

    static class OOMObject {

    }
}

运行结果如下:

Caused by: java.lang.OutOfMemoryError: PermGen space
	at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
    ...

在经常动态生成大量Class的应用中,比如说使用CGLib这类字节码技术的时候,容易出现方法区溢出。所以说在使用这类技术编写框架时,要留意这方面导致的内存溢出。

本机直接内存溢出

本机直接内存的溢出主要与大量的直接操作内存的API有关,比如说NIO相关的API,也可以通过rt.jar中的类使用Unsafe的功能来复现这个异常。DirectMemory容量可通过-XX:MaxDirectMemorySize指定,主要代码如下:

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)
    ...

由DirectMemory导致的内存溢出,不会在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。

原文  http://www.bdqfork.cn/post/217f5df7.html
正文到此结束
Loading...