学习目标:明白虚拟机里面的内存是如何划分的,哪部分区域,什么样的代码和操作可能会导致内存溢出异常。
2.2 运行时数据区域
2.2.1 程序计数器
是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
2.2.2 java 虚拟机栈
每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表,操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配
其中的两种异常情况:1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出stackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
2.2.4 Java堆
他是Java虚拟机所管理的内容中最大的一块。被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例与数组都在这里分配内存。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法在扩展时,将会抛出OutOfMemoryError异常。
2.2.5 方法区
与Java堆一样是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。但是这部分的回收确实是有必要的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
使用句柄访问方式的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。
2.4.1 Java堆溢出
Java堆用于存储对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。
如果不存在泄露,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长到的情况,尝试减少程序运行期的内存消耗。
Java堆内存溢出异常测试
/* * vm Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * */ 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<OOMobject>(); while(true){ list.add(new OOMobject()); } } } } 运行结果: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Unknown Source) at java.util.Arrays.copyOf(Unknown Source) at java.util.ArrayList.grow(Unknown Source) at java.util.ArrayList.ensureExplicitCapacity(Unknown Source) at java.util.ArrayList.ensureCapacityInternal(Unknown Source) at java.util.ArrayList.add(Unknown Source) at HeapOOM$OOMobject.main(HeapOOM.java:11)
虚拟机栈和本地方法栈OOM测试
/* * VM Args: -Xss128k */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak(){ stackLength++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try{ oom.stackLeak(); }catch(Throwable e){ System.out.println("stack length:" + oom.stackLength); throw e; } } } 运行结果: stack length:1000 Exception in thread "main" java.lang.StackOverflowError at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9) at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10) at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10) at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10) at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10) at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
-Xoss参数(设置本地方法栈大小) -Xss参数(设置栈容量)
运行时常量池导致的内存溢出异常
/*VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M */ 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<String>(); //10MB的PermSize在integer范围内足够产生OOM了 int i = 0; while(true){ list.add(String.valueOf(i++).intern()); } } } 运行结果:
程序运行了20分钟,都没有结果