除了PC(程序计数器)以外,Java虚拟机内存区域的都有可能发生OOM(OutOfMemoryError)。
Java堆溢出
Java堆是用于存储对象实例的,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径避免垃圾回收机制被清楚,那么在对象数量到达最大堆的容量限制之后便会产生OOM异常。
解决思路
一般的手段是:先通过内存映像工具对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏还是内存溢出。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。这样就能够找到泄漏的对象是通过怎么样的路径与GC Roots相关联的导致垃圾回收机制无法将其回收。掌握了泄漏对象的类信息和GC Roots引用链的信息,就可以比较准确地定位泄漏代码的位置。
如果不存在泄漏,换句话说,就是内存中的对象确实必须存活着,那么此时就需要通过虚拟机的堆参数( -Xmx和-Xms)来适当调大参数;从代码上检查是否存在某些对象存活时间过长、持有时间过长的情况,尝试减少运行时内存的消耗。
-Xmx : 最大堆空间;
-Xms : 初始堆空间大小,如果初始堆空间耗尽,JVM会对堆空间扩容,其扩展上限为最大堆空间。通常-Xms与-Xmx设置为同样大小,避免扩容造成性能损耗。
虚拟机栈和本地方法栈溢出
由于在HotSpot虚拟机上并不区分虚拟机栈和本地方法栈,因此栈容量只能由-Xss参数设定。在Java虚拟机规范中描述了两种异常:
StackOverflowError :如果线程请求的栈深度超过了虚拟机所允许的最大深度,就会抛出该异常;
OutOfMemoryError:如果虚拟机在拓展栈的时候,无法申请到足够的空间,就会抛出该异常。
对于这两种异常来说,存在着一些互相重叠的地方:当栈空间无法继续分配的时候,到底是内存太小还是已使用的栈空间太大,其实只是对同一件事情的两种描述罢了。
当在单线程环境下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法继续分配的时候,虚拟机抛出的都是StackOverflowError 异常。
在多线程环境下,如果为每个线程的栈分配的内存越大,反而越容易产生OOM异常。每个线程分配到的栈容量越大,可以建立的线程数量就自然减少了,那么在新建立线程的时候就很容易把内存耗尽,产生OOM异常。虚拟机默认参数栈深度大多数情况下能够达到1000~2000,对于正常的方法调用(包括递归)是完全够用的。但是,在多线程环境下的OOM,就只能通过减少最大堆和减少栈容量来换取更多的线程数量。
方法区和运行时常量池溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。当前的一些主流框架,如Spring、Hibernate,对于类进行增强的时候都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成Class可以加载入内存,这样的情况下可能会造成方法区的OOM异常。
在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景还在:大量JSP或动态生成JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视作不同的类)等。
直接内存溢出
直接内存(DirectMemory)容量可以通过-XX : MaxDirectMemorySize指定,如果不指定,则默认与Java最大堆(-Xmx指定)一样。
由于直接内存(DirectMemory)导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,就可以考虑检查一下是否为直接内存(DirectMemory)溢出异常。