jvm,深入理解java虚拟机,实战:OutOfMemoryError异常
Posted 你是我的天晴
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm,深入理解java虚拟机,实战:OutOfMemoryError异常相关的知识,希望对你有一定的参考价值。
在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都
有发生OutOfMemoryError(下文称OOM)异常的可能,本节将通过若干实例来验证异常发生
的场景(代码清单2-3~代码清单2-9的几段简单代码),并且会初步介绍几个与内存相关的
最基本的虚拟机参数。
本节内容的目的有两个:第一,通过代码验证Java虚拟机规范中描述的各个运行时区域
存储的内容;第二,希望读者在工作中遇到实际的内存溢出异常时,能根据异常的信息快速
判断是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这
些异常后该如何处理。
下文代码的开头都注释了执行时所需要设置的虚拟机启动参数(注释中“VM Args”后面
跟着的参数),这些参数对实验的结果有直接影响,读者调试代码的时候千万不要忽略。如
果读者使用控制台命令来执行程序,那直接跟在Java命令之后书写就可以。可以参考下图在Debug/Run页签中的设置。
下文的代码都是基于Sun公司的HotSpot虚拟机运行的,对于不同公司的不同版本的虚拟
机,参数和程序运行的结果可能会有所差别。
Java堆溢出
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达
路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生
内存溢出异常。
代码清单中代码限制Java堆的大小为10MB,不可扩展(将堆的最小值-Xms参数与最
大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:
+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆
转储快照以便事后进行分析
import java.util.ArrayList;
import java.util.List;
//-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM
static class OOMObject
public static void main(String[]args)
List<OOMObject> list=new ArrayList<OOMObject>();
while(true)
list.add(new OOMObject());
//java.lang.OutOfMemoryError:Java heap space
Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出
时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
虚拟机栈和本地方法栈溢出
由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,虽
然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。
关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分成两种情况,看似更加严谨,但却存在着一些互相重叠的地方:当栈空间
无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情
的两种描述而已。
在笔者的实验中,将实验范围限制于单线程中的操作,尝试了下面两种方法均无法让虚
拟机产生OutOfMemoryError异常,尝试的结果都是获得StackOverflowError异常
使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现时输出的
堆栈深度相应缩小。
定义了大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出
StackOverflowError异常时输出的堆栈深度相应缩小。
代码清单虚拟机栈和本地方法栈OOM测试(仅作为第1点测试程序)
/**
*VM Args:-Xss128k
*@author zzm
* 在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常
*/
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:2402
//Exception in thread"main"java.lang.StackOverflowError
实验结果表明:在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无
法分配的时候,虚拟机抛出的都是StackOverflowError异常。
如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常,如
下代码清单所示。但是这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,
或者准确地说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出
异常。
其实原因不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位的Windows
限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内
存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容
量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在
内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到的栈容量越大,可以
建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
这一点读者需要在开发多线程的应用时特别注意,出现StackOverflowError异常时有错误
堆栈可以阅读,相对来说,比较容易找到问题的所在。而且,如果使用虚拟机默认参数,栈
深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说在大多数情
况下)达到1000~2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该完
全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚
拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。如果没有这方面的
处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。
代码清单 创建线程导致内存溢出异常
/**
* /**
*VM Args:-Xss2M(这时候不妨设置大些)
*/
/**
* 注意 特别提示一下,如果读者要尝试运行上面这段代码,记得要先保存当前的工作。
由于在Windows平台的虚拟机中,Java的线程是映射到操作系统的内核线程上的
,因此上述
代码执行时有较大的风险,可能会导致操作系统假死。
Exception in thread"main"java.lang.OutOfMemoryError:unable to create new native thread
*/
public class JavaVMStackOOM
private void dontStop()
while (true)
public void stackLeakByThread()
for (int i = 0; i < 5; i++)
Thread thread=new Thread(new Runnable()
@Override
public void run()
dontStop();
);
thread.start();
public static void main(String[] args)
JavaVMStackOOM oom=new JavaVMStackOOM();
oom.stackLeakByThread();
注意 特别提示一下,如果读者要尝试运行上面这段代码,记得要先保存当前的工作。
由于在Windows平台的虚拟机中,Java的线程是映射到操作系统的内核线程上的[
,因此上述代码执行时有较大的风险,可能会导致操作系统假死。
方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。前
面提到JDK 1.7开始逐步“去永久代”的事情,在此就以测试代码观察一下这件事对程序的实际
影响。
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等
于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包
含的字符串添加到常量池中,并且返回此String对象的引用。在JDK 1.6及之前的版本中,由
于常量池分配在永久代内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大
小,从而间接限制其中常量池的容量,如代码清单2-6所示。
代码清单2-6 运行时常量池导致的内存溢出异常
/**
* /**
* 1.6VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
* 1.7 jdk1.7中的常量池确实是移到了堆中
*/
public class RuntimeConstantPoolOOM
public static void main(String[] args)
使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> strings=new ArrayList<>();
//10MB的PermSize在integer范围内足够产生OOM了
int i=0;
while (true)
strings.add(String.valueOf(i++).intern());
//Exception in thread"main"java.lang.OutOfMemoryError:PermGen space
从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息
是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部
分。
而使用JDK 1.7运行这段程序就不会得到相同的结果,while循环将一直进行下去。。。关于
这个字符串常量池
代码清单2-6在jdk1.7可以运行成功,在jdk1.8运行没反应,是因为永久代(方法区)在JDK 7中逐渐变化,到JDK 8之后完全消失,合并到了Native堆中
在Java7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,但是在之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。Java7中已经将运行时常量池从永久代移除,在Java堆(Heap)中开辟了一块区域存放运行时常量池。而在Java8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域叫元空间。JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。元空间是什么?
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
元空间内存溢出的例子
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* jdk1.8没有永久代了,移除PermSize MaxPermSize 取而代之的是Metaspace
* -XX:MaxMetaspaceSize=10m
*/
public class JavaMaxMetaspace
public static void main(String[] args)
while (true)
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(JavaMethodAreaOOM.OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor()
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
throws Throwable
return methodProxy.invokeSuper(o,objects);
);
enhancer.create();
static class OOMObject
//Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java
堆最大值(-Xmx指定)一样,代码清单2-9越过了DirectByteBuffer类,直接通过反射获取
Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回
实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽然使用
DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申
请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方
法是unsafe.allocateMemory()。
代码清单2-9 使用unsafe分配本机内存
import java.lang.reflect.Field;
/**
*VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
*/
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
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显
的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就
可以考虑检查一下是不是这方面的原因。
以上是关于jvm,深入理解java虚拟机,实战:OutOfMemoryError异常的主要内容,如果未能解决你的问题,请参考以下文章
jvm,深入理解java虚拟机,实战:OutOfMemoryError异常