Java内存溢出基本分析与常用解决方式
在JVM的堆、方法区、Java虚拟机栈、本地方法栈和程序计数器中,除了程序计数器外,其他几个运行时数据区和直接内存都有引发OutOfMemoryError异常的可能。
一、Java堆溢出
Java堆存储的是程序中的对象实例,因此如果不断的有新实例被创建,并且不被垃圾回收,就迟早会造成内存溢出异常。可以通过虚拟机参数-Xmx设置堆的最大值,-Xms设置堆的最小值。
解决堆溢出的第一步是使用-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常的时候导出程序当前的内存堆存储快照,这个内存快照可以方便事后进行分析。
然后通过内存映像分析工具对导出的堆存储快照进行分析,首先应确认内存中导致堆内存溢出的对象是否是程序运行所必须的,分清楚是出现了内存泄漏还是内存溢出。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,弄清楚泄漏对象是怎样的引用路径才导致垃圾收集器没有对其进行回收。如果是内存溢出,则考虑通过-Xmx参数增大堆内存的最大值。
二、虚拟机栈和本地方法栈内存溢出
虚拟机栈和本地方法栈都是方法调用时分配内存的区域,该区域存在两种异常,一是如果线程请求的栈深度大于虚拟机所允许的最大深度或无法满足新栈帧的内存分配需求,将抛出*Error异常,二是如果虚拟机的栈允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。是否允许动态扩展由虚拟机自行决定,如HotSpot虚拟机就选择的不支持扩展。
是否对虚拟机栈和本地方法栈进行区分也由虚拟机自行决定的,对于进行区分的虚拟机可通过-Xoss参数设置本地方法栈的大小,而不区分的只能由-Xss参数设置栈容量大小。
三、方法区和运行时常量池内存溢出
自JDK7开始,原本存放在永久代的字符串常量池被移至Java堆之中。
public class RuntimeTest{
String str1 = new StringBuilder("computer").append("software").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);
}
上述代码在JDK6中运行会输出两个false,而在JDK7中则输出一个false,一个true,这是因为在JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是这个常量池中的引用,而StringBuilder创建的字符串对象是存储在堆内存中的,所以必然不是同一个引用;而在JDK7中,字符串常量池已经移到堆内存中,因此intern()返回的引用和StringBuilder创建的那个字符串实例引用就是同一个,而str2返回false是因为Java这个字符串已经存在与字符串常量池中。
方法区主要用于存储类的相关信息,如类名、访问修饰符、常量池、字段描述等。因此如果有大量的类,方法区就极有可能会产生内存溢出异常。如Spring、Hibernate等对类进行增强时,都会使用到CGLib进行动态代理,产生代理类,当代理类越多,就需要越大的方法区来保证动态生成的新类可以载入到虚拟机中。
在JDK8以后,永久代已完全退出历史舞台,元空间作为其代替,动态的创建新类已经很难使方法区产生内存溢出异常。关于元空间有以下几个配置参数:
- -XX:MaxMetaspaceSize: 设置元空间的最大值,默认是-1,即不限制;
- -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如释放了大量的空间就对其进行调小,反之则调大;
- -XX: MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。
四、直接内存溢出
直接内存的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不指定,则默认与Java堆的最大值一致。由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况。