本文是基于周志明的《深入理解Java虚拟机》
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
//VM Args: -Xss128k
//Java堆溢出异常测试
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List list = new ArrayList();
while (true) {
list.add(new OOMObject());
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space |
处理方式:
1).内存泄露虽然分了两种情况,其实存在互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质只是对同一件事情的两种描述而已。
/**
* VM Args: -Xss128k
*
* 1.使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError,
* 异常出现时输出的堆栈尝试相应缩小。
* 2.定义了大量的本地变量,增大此方法帧中本地变量表的长度。
* 结果:抛出StackOverflowError,异常出现时输出的堆栈尝试相应缩小。
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e){
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
stack length:11411Exception in thread "main" java.lang.StackOverflowError at com.changwen.javabase.JVM.OutOfMemoryError.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12) at com.changwen.javabase.JVM.OutOfMemoryError.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13) at com.changwen.javabase.JVM.OutOfMemoryError.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13).......(最后程序还是会停止的) |
如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常。但是这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,准确地说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出 异常。所以在多线程开发的应用时需要特别注意,如果出现StackOverflowError异常时有错误堆栈可以阅读,相对来说,比较容易找到错误问题所在。
/**
* VM Args: -Xss2M(这时候不妨设置大些)
*
* 如果要尝试运行上面这段代码,记得要先保存当前的工作。
* 由于在Windows平台的虚拟机中,Java的线程是映射到操作系统的内核线程上的,
* 因此上述代码执行时有较大的风险,可能会导致操作系统假死。
*/
public class JavaVMStackOOM {
private void dontStop() {
while(true){}
}
public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native method |
由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。由于JDK1.7开始逐步“去永久代”,所有下面的测试一定要注意JDK的版本。
String.intern()是一个Native方法,它的作用:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。
在JDK1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过-XX:PermSize和MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @version jdk1.6
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//使用List保持着常量池引用,避免Full GC回收常量池行为
List list = new ArrayList();
//10M的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) |
----------------------------------------------------------------------------------------------------------
虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory().
/**
* VM Args -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
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(_1MB);
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) |