笔者在上一篇文章讲了JVM学习笔记-内存泄漏异常一,针对的是java堆和虚拟机栈、本地方法栈内存溢出的情况。在这片文章中,将讲述方法区、运行时常量池溢出和直接内存溢出两种情况。
1、方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试将放在一起进行。
提到运行时常量池,会经常想到String.intern()方法。该方法的作用是:若字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。在jdk1.6版本前,由于常量池分配在永久代中,只需要设置-XX:PermSize和
-XX:MaxPermSize的大小来限制方法区的大小,从而间接的限制其中常量池的大小。以下示例用来测试运行时常量池导致的内存溢出情况:
/**
* VM 参数:-XX:PermSize=10M -XX:MaxPermSize=10M -Xms20m -Xmx20m
*
* @author sj
*
*/
public class ConstantPoolOOMTest {
public static void main(String[] args) {
// 使用list保持常量池的引用,避免垃圾回收
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
在jdk1.7版本以上时,报错如下:
而在1.6版本时,报错提示的区域不一样:
这也就验证了,1.7版本后,运行时常量池由永久代移到了java堆中。
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。因此对于方法区溢出的测试,基本思路就是运行时产生大量的类去填满方法区,直到溢出。在许多主流框架比如Spring、Hibernate中,都会用到CGLib技术对类进行增强,增强的类越多,就需要越大的方法区来保证动态生成的Class可以载入内存。下面就借助CGLib直接操作字节码运行时生成大量的动态类:
/**
* VM 参数:-XX:PermSize=10M -XX:MaxPermSize=10M
*
* @author sj
*
*/
public class MethodAreaOOMTest {
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 {
}
}
运行结果:
方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾回收,条件是比较苛刻的。在经常动态生成大量class的应用中,需要特别关注垃圾回收情况。这类场景除了是上面所说的使用CGLib字节码增强和动态语言外,常见的还有:大量JSP或者动态产生JSP文件的应用、基于OSGI的应用等。
2、直接内存溢出
设置直接内存的VM参数:
-XX : MaxDirectMemorySize
若不指定该参数,则直接内存的大小默认与Java堆的最大值一样,下面代码使用unsafe分配本机内存:
/**
* VM 参数: -Xmx20M -XX:MaxDirectMemorySize=10M
*
* @author sj
*
*/
public class DirectMemoryOOMTest {
// 每次分批内存大小为1MB
private static final int OneMB = 1024 * 1024;
public static void main(String[] args) throws IllegalArgumentException,
IllegalAccessException {
// 直接通过反射获取到Unsafe实例
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
// 申请分配内存
unsafe.allocateMemory(OneMB);
}
}
}
运行结果:
由直接内存导致的内存溢出,一个明显的特征就是在Head Dump文件中不会看见明显异常,若发现OOM之后Dump文件很小,并且程序中又直接或间接使用了NIO,有可能就是这方面的原因。
3、总结
JVM学习笔记-内存泄漏异常一和JVM学习笔记-内存泄漏异常二两篇文章主要讲了哪部分区域、什么样的代码和操作可能导致内存溢出。即使Java有垃圾回收机制,但我们在日常开发中还是会经常遇到内存溢出的情况。
参考:
深入理解Java虚拟机第2章-周志明