此篇是上一篇文章 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,那就可以考虑检查一下是不是这方面的原因。