程序计数器
用于给字节码解释器来选取吓一跳需要执行的字节码指令。每个线程有一个独立的程序计数器去,且各个线程之间互不影响。如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的内存地址;如果执行的是Native方法。在计数器为Undefined。此区域是JVM规范中唯一一个不存在OOM的区域
虚拟机栈(局部变量空间)
存放编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象应用(reference)。64位的double、long占用2个槽。内存空间在编译期间就能确定,当进入一个方式时,这个方法需要分配的局部变量空间是完全确定的,通过-Xss设置内存容量
异常状况:
StackOverflowError 栈深度大于虚拟机所允许的深度
OOM 如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可以动态扩展,只不过Java虚拟机规范中的也允许固定长度的虚拟机栈),如果扩展是无法申请到足够的内存
在单个线程下,无论是犹豫栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError
/** * VM Args:-Xss128k * * stack length:2402 Exception in thread "main" java.lang.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; } } }
如果测试时不限于单线程,通过不断的建立线程的方式到是可以产生内存溢出的异常。但是这样产生的内存溢出异常与占空间是否足够大并不存在任何联系,或者准确的说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出的异常。
因为操作系统分配给每个进程的内存是有限制的,譬如32的window限制为2GB。此测试是通过创建大量的线程。每个线程占用栈内存分配大量的内存,导致系统没有足够的内存。使其不能自动扩展
/** * VM Args:-Xss2M (这时候不妨设大些) * * java.lang.OutOfMemoryError:unable to create new native thread */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(); } }
本地方法栈
跟虚拟机栈类似,只是一个是虚拟机执行Java方法,一个是执行Native方法
异常状况:
StackOverflowError 栈深度大于虚拟机所允许的深度
OOM
Java堆
线程共享的一块内存区域,从内存回收角度来看,基本都采用分代收集算法,所以分为新生代、老年代。再细致一点可以分为Eden空间、From Survivor空间、To Survivor空间等。-Xmx -Xms控制堆空间大小
异常状况:
1.OOM 堆无法扩展时
/** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * * java.lang.OutOfMemoryError: Java heap space */public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } }
方法区
线程间共享。存储已经被虚拟机加载的类信息、常量、静态变量、即时编辑器编译后的代码等数据,在HotSpot虚拟机中可以称为永生代。
运行时常量在1.6及之前是方法区的一部分(String.intern()动态加入常量池) -XX:MaxPermSize控制大小。在JDK1.7及之后的版本是在Java堆中开辟的一块内存
异常状况:
OOM
/** * 需要在JDK1.6上才能复现,JDK1.7及之后版本的JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。 * 在JDK1.7上运行的效果则会一直执行,直到堆内存使用完毕 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * * java.lang.OutOfMemoryError:PermGen space */public class RuntimeConstantPoolOOM {public static void main(String[] args) { // 使用List保持着常量池引用,避免Full GC回收常量池行为 List<String> list = new ArrayList<String>(); // 10MB的PermSize在integer范围内足够产生OOM了 int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } }
/** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M * java.lang.OutOfMemoryError:PermGen space * 一直创建动态类 */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() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } }static class OOMObject { } }
直接内存(不属于虚拟机运行时的数据区的一部分)
NIO可以使用Native函数库直接分配对外的内存,然后通过存储在Java对中的DirectByteBuffer对象作为这块内存的引用进行操作。受限于机器物理内存,可以通过-XX:MaxDirectMemorySize制定,如果不制定,默认与Java堆最大值(-Xmx)一样
异常状况:
1.OOM
/** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * * java.lang.OutOfMemoryError */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); } } }