堆溢出
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径避免垃圾回收,当到达最大堆的容量限制后就会产生Java.lang.OutOfMemoryError.
/**
* VM Options:
* -Xms20M
* -Xmx20M
* -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM{
static class OOMObject{}
public static void main(String[] args){
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
结果:
GC多次执行后触发OutOfMemoryError.
栈溢出
关于虚拟机栈,在Java规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
然而,在单线程下,虚拟机在栈空间不足时会尝试扩展栈空间,因此,当无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其实是一回事。在实验中,单线程环境下,只会抛出StackOverflowError异常。
/**
* VM Option:
* -Xss160K
*/
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;
}
}
}
结果:
操作系统分配给每个进程的内存是有限制的,通常为操作系统限制总内存-最大堆容量(Xmx)-最大方法区容量(MaxPermSize)-程序计数器消耗
。每个线程分配到的栈容量越大,可以建立的线程数目越小。
/**
* VM Options:
* -Xss2M
*/
public class JavaVMStackOOM{
private void dontStop(){
while(true){}
}
public void stackLeakByThread(){
while(true){
Thread thread = new Thread(new Runnable(){
@Override
public void run(){
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable{
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
结果:Exception in thread "main" java.lang.outOfMemoryError: unable to create new native thread
运行时常量池溢出
运行时常量池在JDK 1.6及之前版本中在方法区中,在1.7及之后转移至堆空间。在JDK 1.6及之前版本中可以通过限制方法区大小,从而间接限制运行时常量池大小。
/**
* ONLY WORKS BEFORE JDK 1.7
* VM Options:
* -XX:PermSize=10M
* -XX:MaxPermSize=10M
public class RuntimeConstantPoolOOM{
public static void main(String[] args){
List<String> list = new ArrayList<String>();
int i = 0;
while(true){
list.add(String.valueof(i++).intern());
}
}
}
结果:Exception in thread "main" java.lang.OutOfMemoryError:PermGen space
方法区溢出
方法区用于存放Class相关信息,因此要使得方法区溢出,除了在JDK 1.7之前使运行时常量池溢出外,基本的思路是运行时生成大量的类去填满方法区。
结果Exception in thread "main" java.lang.OutOfMemoryError:PermGen space
直接内存(DirectMemory)溢出
直接内存不是虚拟机运行时数据区的一部分。在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,使用Native函数库直接分配堆外内存。
DirectMemory容量可通过-XX: MaxDirectMemorySize
指定,如果不指定,则默认与Java堆最大值一样(-Xmx)。直接通过allocateMemory可以造成本机内存溢出。
结果: Exception in thread "main" java.lang.OutOfMemoryError
直接内存溢出的一个特征是Heap Dump文件中不会看先明显的异常指示。如果OOM之后Dump文件很小,而程序中又直接或间接使用了DIO,就应该检查是否直接内存溢出。
String.intern()
String.intern()是一个Native方法,作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
public class RuntimeConstantPoolOOM{
public static void main(String[] args){
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);//JDK 1.6 false JDK 1.7 true
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);//JDK 1.6 false JDK 1.7 true
}
}
在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代(方法区运行时常量池),返回的是这个永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用。
在JDK 1.7中,intern()实现不会再复制,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。
小节
内存区域 | 描述 | VM Option | 异常 |
---|---|---|---|
程序计数器 | 略 | 略 | 略 |
虚拟机栈 | 存放编译器可知的各种基本类型,对象引用和returnAddress类型 | -Xss160K 每个线程的栈大小 | StackOverflowError/OutOfMemoryError |
Java堆 | 存放对象实例 | -Xms10M 最大值 -Xmx20M 最小值 |
OutOfMemory: Java heap space |
运行时常亮池 | 存放编译期生成的字面量和符号引用,运行期也能放入常量池(string.intern())。JDK 1.7之前在方法区中,JDK 1.7及之后移至堆中 | 随方法区或堆设置 | OutOfMemoryError |
方法区 | 存储虚拟机加载的类信息、常亮、静态变量、即时编译器编译后的代码等数据,又称为永久代(Permanent Generation) | -XX:PermSize=10M 初始值 -XX:MaxPermSize=20M 最大值 |
OutOfMemoryError: PermGen space |
直接内存 | 在JDK 1.4中加入NIO类,直接分配堆外内存 | -XX:MaxDirectMemorySize=10M, 如果不指定默认与-Xmx一样 |
OutOfMemoryError |
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。