java运行时数据区中除了程序计数器外,其它区域都有可能出现内存溢出(OOM)的异常,本文就主要介绍java运行时可能出现内存溢出的各区域,以便在出现内存溢出时能够快速的判断出代码哪里可能出现问题。

1、虚拟机堆出现OOM

       虚拟机堆内存是java内存区域中占内存最大的一块,当堆需要分配内存给对象完成实例化时,如果没有足够内存,并且堆无法再扩展时,将抛出OOM,其实在JVM抛出OOM之前,GC会马上进行一次内存回收,如果回收之后仍旧内存不足就会抛出OOM。虚拟机堆区域引起的OOM会抛出“java.lang.OutOfMemoryError: Java heap space”。

       堆内存大小可以通过虚拟机参数-Xms与-Xmx来控制,即堆的最小值与堆的最大值,如果你把这两个值设置的一样大的话,那么堆内存就不会自动扩展了,同时通过参数-XX:+HeapDumpOnOutOfMemoryError可以让JVM在OOM时Dump出内存堆转储快照文件来进行分析。堆出现OOM时,可能是代码不当造成内存溢出,也有可能是内存泄漏导致内存不足,所遇内存泄漏就是一些本应该被GC回收的对象没有被回收,导致这些无用的对象“霸占”了内存空间导致内存不足。

2、栈溢出

       上一篇文章有提到HotSpot将虚拟机栈与本地方法栈合二为一了,在java虚拟机栈的规范中栈有两种异常,StackOverflowError和OutOfMemoryError。其中如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError。如果虚拟机在扩展栈时申请不到足够的内存空间就会抛出OutOfMemoryError。栈的容量可以通过-Xss参数来设置。

       其实这里之前我也是有点疑惑的,你说这栈深度过大,即栈帧数过多和栈扩展时申请不到足够的内存这两种情况不是一种情况吗?那到底是栈过大还是栈内存不够怎么区分呢?不知能你们在学习这块的时候有没有这种疑问,我在周志明的《深入理解java虚拟机》一书中看到了较为详细的描述。理解这一点需要理解栈容量,首先栈是线程私有的,一个线程的栈容量就是这个线程栈所允许的最大深度,当你超出了这个深度JVM就会抛StackOverflowError,而OOM是当JVM一时间执行了很多线程,导致每个线程要获取栈容量的总和超出了物理内存时,就会抛出OutOfMemoryError异常。

3、方法区溢出

       上文有介绍到方法区,方法区中存有类信息、静态变量以及常量池等等。在JDK1.8之前是在内存中划出一块区域来存储定义在方法区的这些变量的,但是呢在JDK1.8JVM的架构有了一些改动,以前划给方法区的这块内存区域没有了,方法区定义的静态变量、常量池等部分数据放到了java堆内存里,那这些也就会造成java堆内存溢出了,所以有人就说方法区是堆的一部分了。我觉得不对哦,第一:方法区中定义的变量并不是都放到了java堆里面,像类信息就被直接放到了本地内存,这个本地内存就是JVM占用的物理内存以外的物理内存。类信息占用的内存空间也被称为元空间,其大小就仅受物理内存限制了。第二:在JDK1.8之前,方法区溢出抛出的异常是“java.lang.OutOfMemoryError:PermGen space”(HotSpot虚拟机),在1.8之后元空间出现溢出会抛出“java.lang.OutOfMemoryError:Metaspace”以此来代替PerGen space。

       因此方法区在概念上依旧是存在的,这个区域定义的变量也还都存在,一个没少,只是实现上有变动而已。我觉得这样做是有道理的,但是我不知道java设计者们是怎么想的。1.8的方法区实现改动的确能解决一些问题,因为在现在的很多主流框架中,特别是spring生态,大量的用到了动态代理,生成了很多的动态类,代理类,这些类变量被放到了本地内存中,堆中,能利用的空间就更多了,如果按照1.8之前划分一块区域来存这些代理类就可能因内存不够经常出现Full GC的问题。

       上一篇文章中有介绍到直接内存,直接内存使用的也是本地内存。上文提到的元空间使用本地内存默认是没有限制的,只要不超过最大物理内存就行,而直接内存容量是受虚拟机规范限制的,默认和虚拟机堆最大值一样,可以通过-XX:MaxDirectMemorySize来设置,因此直接内存也是会出现OOM异常的。