上篇学习到虚拟机如何对象的概念,其中提到基础OutOfMemoryError异常,本篇具体学习下。
在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能,本篇将通过若干实例来验证异常实际发生的代码场景,并且将初步介绍若干最基本的与自动内存管理子系统相关的HotSpot虚拟机参数。
本篇实战的目的有两个:第一,通过代码验证《Java虚拟机规范》中描述的各个运行时区域储存的内容;第二,希望读者在工作中遇到实际的内存溢出异常时,能根据异常的提示信息迅速得知是哪个区域的内存溢出,知道怎样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理。
1、Java堆溢出
Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
案例:代码如下(包含设置vm参数)
/**
* -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMOBject{
}
public static void main(String[] args) {
List<OOMOBject> oomoBjects = new ArrayList<>();
while (true){
oomoBjects.add(new OOMOBject());
}
}
}
异常信息如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid6024.hprof ...
Heap dump file created [30008039 bytes in 0.095 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3720)
at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:242)
at java.base/java.util.ArrayList.add(ArrayList.java:485)
at java.base/java.util.ArrayList.add(ArrayList.java:498)
at com.mxy.algorithm.JVM.HeapOOM.main(HeapOOM.java:17)
Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heapspace”。
要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如EclipseMemory Analyzer或者idea的JProfiler)对Dump出来的堆转储快照进行分析。第一步首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
- 内存泄漏:可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
- 不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
以上是处理Java堆内存问题的简略思路,处理这些问题所需要的知识、工具与经验后面会逐步学习,后面我们将会针对具体的虚拟机实现、具体的垃圾收集器和具体的案例来进行分析,这里就先暂不展开。
2、虚拟机栈和本地方法栈溢出
由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
为了验证这点,我们可以做两个实验,先将实验范围限制在单线程中操作,尝试下面两种行为是否能让HotSpot虚拟机产生OutOfMemoryError异常:
- 使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。
- 定义了大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。
第一种StackOverflowError
public class JavaVMStackSOF {
private int stackLength=1;
//递归
private void satckLeak()throws Exception{
stackLength++;
satckLeak();
}
public static void main(String[] args) throws Exception {
JavaVMStackSOF stackOOM = new JavaVMStackSOF();
try {
stackOOM.satckLeak();
} catch (Exception e) {
System.out.println("stack length:"+stackOOM.stackLength);
throw e;
}
}
}
错误信息:
Exception in thread "main" java.lang.StackOverflowError
at com.mxy.algorithm.JVM.JavaVMStackSOF.satckLeak(JavaVMStackSOF.java:11)
at com.mxy.algorithm.JVM.JavaVMStackSOF.satckLeak(JavaVMStackSOF.java:11)
第二种:StackOverflowError
局部变量定义过多超出局部变量表空间大小
public static void main(String[] args) throws Exception {
try {
test();
} catch (Exception e) {
System.out.println("stack length:" +stackLength);
throw e;
}
}
public static void test() {
long number01, number02, number03, number04, number05, number06, number07, number08, number09, number10, number11, number12, number13, number14, number15,
number16, number17, number18, number19, number20, number21, number22, number23, number24, number25, number26, number27, number28, number29, number30,
number31, number32, number33, number34, number35, number36, number37, number38, number39, number40, number41, number42, number43, number44, number45,
couber01, couber02, couber03, couber04, couber05, couber06, couber07, couber08, couber09, couber10, couber11, couber12, couber13, couber14, couber15,
couber16, couber17, couber18, couber19, couber20, couber21, couber22, couber23, couber24, couber25, couber26, couber27, couber28, couber29, couber30,
couber31, couber32, couber33, couber34, couber35, couber36, couber37, couber38, couber39, couber40, couber41, couber42, couber43, couber44, couber45,
agrber01, agrber02, agrber03, agrber04, agrber05, agrber06, agrber07, agrber08, agrber09, agrber10, agrber11, agrber12, agrber13, agrber14, agrber15,
agrber16, agrber17, agrber18, agrber19, agrber20, agrber21, agrber22, agrber23, agrber24, agrber25, agrber26, agrber27, agrber28, agrber29, agrber30,
agrber31, agrber32, agrber33, agrber34, agrber35, agrber36, agrber37, agrber38, agrber39, agrber40, agrber41, agrber42, agrber43, agrber44, agrber45,
dsaber01, dsaber02, dsaber03, dsaber04, dsaber05, dsaber06, dsaber07, dsaber08, dsaber09, dsaber10, dsaber11, dsaber12, dsaber13, dsaber14, dsaber15,
dsaber16, dsaber17, dsaber18, dsaber19, dsaber20, dsaber21, dsaber22, dsaber23, dsaber24, dsaber25, dsaber26, dsaber27, dsaber28, dsaber29, dsaber30,
dsaber31, dsaber32, dsaber33, dsaber34, dsaber35, dsaber36, dsaber37, dsaber38, dsaber39, dsaber40, dsaber41, dsaber42, dsaber43, dsaber44, dsaber45;
stackLength++;
test();
number01=number02=number03=number04=number05=number06=number07=number08=number09=number10=number11=number12=number13=number14=number15=0;
number16=number17=number18=number19=number20=number21=number22=number23=number24=number25=number26=number27=number28=number29=number30=0;
number31=number32=number33=number34=number35=number36=number37=number38=number39=number40=number41=number42=number43=number44=number45=0;
couber01=couber02=couber03=couber04=couber05=couber06=couber07=couber08=couber09=couber10=couber11=couber12=couber13=couber14=couber15=0;
couber16=couber17=couber18=couber19=couber20=couber21=couber22=couber23=couber24=couber25=couber26=couber27=couber28=couber29=couber30=0;
couber31=couber32=couber33=couber34=couber35=couber36=couber37=couber38=couber39=couber40=couber41=couber42=couber43=couber44=couber45=0;
agrber01=agrber02=agrber03=agrber04=agrber05=agrber06=agrber07=agrber08=agrber09=agrber10=agrber11=agrber12=agrber13=agrber14=agrber15=0;
agrber16=agrber17=agrber18=agrber19=agrber20=agrber21=agrber22=agrber23=agrber24=agrber25=agrber26=agrber27=agrber28=agrber29=agrber30=0;
agrber31=agrber32=agrber33=agrber34=agrber35=agrber36=agrber37=agrber38=agrber39=agrber40=agrber41=agrber42=agrber43=agrber44=agrber45=0;
dsaber01=dsaber02=dsaber03=dsaber04=dsaber05=dsaber06=dsaber07=dsaber08=dsaber09=dsaber10=dsaber11=dsaber12=dsaber13=dsaber14=dsaber15=0;
dsaber16=dsaber17=dsaber18=dsaber19=dsaber20=dsaber21=dsaber22=dsaber23=dsaber24=dsaber25=dsaber26=dsaber27=dsaber28=dsaber29=dsaber30=0;
dsaber31=dsaber32=dsaber33=dsaber34=dsaber35=dsaber36=dsaber37=dsaber38=dsaber39=dsaber40=dsaber41=dsaber42=dsaber43=dsaber44=dsaber45=0;
}
无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。可是如果在允许动态扩展栈容量大小的虚拟机上,相同代码则会导致不一样的情况。
每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
3、方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。前面提到HotSpot从JDK7开始逐步“去永久化”的计划,并在JDK 8中完全使用元空间来代替永久代的背景故事,在此我们就以测试代码来观察一下,使用“永久代”还是“元空间”来实现方法区,对程序有什么实际的影响。
String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。代码如下:
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
short i=0;
while (true){
set.add(String.valueOf(i++).intern());
}
}
运行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
4、本机直接内存溢出
直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致,如下代码越过了DirectByteBuffer类直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在JDK 10时才将Unsafe的部分功能通过VarHandle开放给外部使用),因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe::allocateMemory()。
public class DirectMemoryOOM {
private static final int _1MB=1024*1024;
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true){
unsafe.allocateMemory(_1MB);
}
}
}
结果如下:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.mxy.algorithm.JVM.DirectMemoryOOM.main(DirectMemoryOOM.java:18)
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。