在 Java 虚拟机规范的描述中,除了程序计数器外,虚拟机的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能。
前言 一本好书,每读一遍都会有不同的感受。写读书笔记,一来是便于平时查阅,毕竟技术书籍都比较厚,不方便随时携带;二来是督促自己多读书,理论与实践结合才能不断提升自己。
【深入理解Java虚拟机】阅读笔记: 2.4 OutOfMemoryError 异常
Java 堆溢出 Java 堆用于存储对象实例,只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径,避免垃圾回收机制清除这些对象,那么在达到最大堆的容量限制后就会产生内存溢出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HeapOOM { static class OOMObject { } public static void main (String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true ) { list.add(new OOMObject()); } } }
虚拟机栈和本地方法栈溢出 Java 虚拟机规范描述的两种异常:本质上是堆同一件事情的两种描述
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常 如果虚拟机在扩展栈时无法申请到足够的内存空间, 则抛出 OutOfMemoryError 异常 实验情况:
使用 -Xss 参数减少栈内存的容量。结果:抛出 StackOverflowError 异常 定义了大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出 StackOverflowError 异常 如下为第 1 点测试程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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; } } }
创建线程导致内存溢出异常:通过不断地建立线程产生内存溢出异常,这样产生的内存溢出异常与栈空间是否足够大不存在联系。相反,为每个线程的栈分配的内存越大,越容易产生内存溢出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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(); } }
方法区和运行时常量池溢出 运行时常量池是方法区的一部分
JDK 1.6 及之前的版本中,常量池分配在永久代,可通过-XX:PermSize 和 -XX:MaxPermSize 直接限制其容量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 中,intern() 方法会把首次遇到的字符串实例复制到永久代中,返回其引用 在 JDK 1.7 中,intern() 不会复制实例,只是在常量池中记录首次出现的实例引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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); } }
方法区用于存放 Class 的相关信息,测试的基本思路是运行时产生大量的类去填满方法区,直到溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class JavaMethodAreaOOM { static class OOMObject { } 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(); } } }
本机直接内存溢出 使用 DirectByteBuffer 分配内存导致抛出内存溢出异常时,实际并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,手动抛出异常 直接通过反射获取 Unsafe 实例,调用 unsafe.allocateMemory() 可直接向系统进行内存分配 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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); } } }
参考 周志明. 深入理解Java虚拟机