Java虚拟机(4)OutOfMemoryError异常
在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
Java堆溢出
Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。
产生原因
Java堆用于储存对象实例,不断地创建对象,总容量触及最大堆的容量限制后就会产生内存溢出异常。
处理方法
首先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,确认是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
- 内存泄漏
- 通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GCRoots相关联,才导致垃圾收集器无法回收它们;
- 根据泄漏对象的类型信息以及它到GC Roots引用链的信息, 定位到这些对象创建的位置, 找出产生内存泄漏的代码 。
- 不是内存泄漏(内存中的对象确实都是必须存活的)
- 检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比, 是否还有向上调整的空间;
- 代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
虚拟机栈和本地方法栈溢出
关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:
-
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
-
如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
方法区和运行时常量池溢出
HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代,原本存放在永久代的字符串常量池被移至Java堆之中。
用以下一段代码来讲述区别:
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);
}
}
结果,在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。
原因是,在JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder
创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。
而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例到永久代了。 字符串常量池已经移到Java堆中, 只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder
创建的那个字符串实例就是同一个。
对str2比较返回false,这是因为"java"这个字符串在执行StringBuilder.toString()
之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则。而“计算机软件”这个字符串则是首次出现的,因此结果返回true。
本机直接内存溢出
由直接内存导致的内存溢出,一个明显的特征是在HeapDump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小, 程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。
欢迎点赞/评论,你们的赞同和鼓励是我写作的最大动力!