在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能。
在Eclipse中进行JVM参数设置
可以直接通过上方菜单栏的 Run 下的 Run Configurations... 或者 Debug Configurations... 选项进行设置。
双击 Java Application 会生成一个新的配置卡。
在新的配置卡中可配置名称Name,Main里面的Project配置项目名,Main class配置main方法所在的类。
(如果是从main方法里面右键打开的Run As-->Run Configurations...或者Debug As-->Debug Configurations...,这几项会自己生成)
Arguments下面的VM arguments 里面配置虚拟机参数。
参数解释:
1,堆是存储对象实例的,创建的对象都是在堆中进行内存分配的;设置堆的大小:-Xmx20M(最大值) ,-Xms20M(最小值),其中-Xmn设置年轻代大小。
2,栈是存储局部变量,操作栈,动态链接,方法出口(都在栈桢中) 的地方,调用方法时,会创建栈桢;设置栈的大小:-Xss128K。
3,方法区是存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。此外运行时常量池是属于方法区的,即存放常量,静态常量等;设置方法区大小,-XX:PermSize=10M和-XX:MaxPermSize=10M。
4,本地直接内存;设置本地直接内存大小:-XX:MaxDirectMemorySize(默认与-Xmx的值一样)。
堆内存溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
参数设置:-verbose:gc -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
限制Java堆的大小为20M,不可扩展(将堆的最小值 -Xms 参数和最大值 -Xmx 参数设置一样即可避免堆自动扩展),通过参数 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时 Dump 出当前的内存堆转储快照以便事后进行分析。
代码:
package jvm; import java.util.ArrayList;
import java.util.List; public class HeapOOM { static class OOMObject {
} public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>(); while (true) {
list.add(new OOMObject());
} } }
运行结果:
栈内存溢出
在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,所以 -Xoss 参数无效,栈容量只由 -Xss 参数控制。
参数设置:-Xss128k
代码:
package jvm; 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;
} } }
运行结果:
方法区内存溢出
参数设置:-XX:PermSize=10M -XX:MaxPermSize=10M
注意:JDK8中用 MetaspaceSize 代替 PermSize,因此将 -XX:PermSize=10M -XX:MaxPermSize=10M 修改为 -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
String.intern() 方法是一个Native方法,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
代码一:
package jvm; import java.util.ArrayList;
import java.util.List; public class RuntimeConstantPoolOOM { public static void main(String[] args) {
// 使用List保持常量池引用,避免 Full GC 回收常量池行为
List<String> list = new ArrayList<>();
// 10MB的 PermSize在Integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
} } }
以上代码在JDK1.6及之前的版本下,由于常量池分配在永久带内,运行时常量池溢出,使用JDK1.7以及更高版本运行这段程序不会得到相同结果,while循环将一直进行下去。因为1.7及更高版本的常量池值只保存了引用。
代码二:
package jvm; 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); } }
运行结果:
结果分析:
JDK1.6会得到两个false。
在JDK1.6中,intern() 方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久带中这个字符串实例的引用。而 StringBuilder 创建的字符串实例在堆上,所以不是同一个引用,返回 false。
JDK1.7中会得到一个true和一个false。
在JDK1.7中,intern() 方法不会再复制实例,只在常量池中记录首次出现的实例引用,该引用指向的是堆中的实例,这个实例就是 StringBuilder 创建的那个字符串实例,所以是同一个引用,返回true。
返回false是因为 “java” 这个字符串在中StringBuilder之前已经出现过,字符串常量池中已经有它的引用了,而 StringBuilder 又重新创建了一个“java”字符串实例,所以str2中保存的是新创建的实例的引用,而str2.intern() 返回的是旧的实例的引用,所以不是同一个引用,返回false。
代码三:
package jvm; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy; public class JavaMethodAreaOOM { public static void main(String[] args) { while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOmObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
}); enhancer.create(); } } static class OOmObject {
} }
以上代码使用使用CGLib使方法区出现内存溢出异常。使用到下面的两个jar包。
JDK8运行结果:
本机直接内存溢出
参数设置:-Xmx20M -XX:MaxDirectMemorySize=10M
直接内存容量可通过 MaxDirectMemorySize 指定,如果不指定,则默认和Java堆最大值一样(-Xmx)。
代码:
package jvm; import java.lang.reflect.Field; import sun.misc.Unsafe; 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);
}
} }
运行结果: