引言
本文总结自周志明的《深入理解Java虚拟机》第二章部分内容。
这部分内容,可以为后续性能调优方面的工作起到铺垫作用。
一、什么是 OutOfMemoryError
OurOfMemory 简称“OOM”, 直译为“内存耗尽”或“内存溢出”,当然,并不是真的内存耗尽了,它指的是 JVM 的几个逻辑分区的内存不够用了,无法为新的对象分配空间。
在 JVM 的几个主要内存分区中(JVM 栈、本地方法栈、计数器、方法区、Java 堆),只有计数器不会出现这种严重错误,也就是说,我们常说的堆和栈等,都有可能出现 OOM 的问题。
二、Java 堆溢出
由于 Java 中最常见的内存分配就是对象,因此,经常要分配内存用于创建对象的堆区,是出现OOM问题的最常见内存分区。
对于 Java 堆的内存溢出,原因其实非常简单。因为堆是用于存储对象的,因此只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径避免垃圾回收机制清除这些对象,那么堆就必然会出现 OOM 问题。
如果要试验堆上的 OOM ,最快的方法就是将堆的分配大小调的低一些,并且不可扩展。
跟在 java 启动指令之后的两个最基本的堆大小分配参数是:-Xms 最小堆内存 和 -Xmx 最大堆内存。将这两个参数的值设置为相同,即可避免堆内存的自动扩展。
案例演示
案例使用 JDK 1.8 ,IDE是 Eclipse,内存分析工具Eclipse Memory Analyzer(简称 MAT)(https://www.eclipse.org/mat/downloads.php)
使用如上图所示的虚拟机参数执行程序,即可发生堆内存的 OOM 错误。-XX:+HeapDumpOnOutOfMemoryError 参数可以让虚拟机在出现内存溢出时Dump(转储;倾倒)出当前的内存堆转储快照,以便事后进行分析。
dump文件的名称类似: java_pid31228.hprof,需要使用 MAT 打开并分析。(具体分析过程暂时不做详细介绍)
总之,当出现堆溢出时,一般的手段是,先通过内存映像分析工具(如Eclipse Memory Analyzer)对 Dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
内存泄漏,表示对象已经无用,但是GC并没有回收,需要进一步通过工具查看泄漏对象到 GC Roots 的引用链,掌握了泄漏对象的类型信息和 GC Roots 引用链 信息,就可以比较准确地定位出泄漏代码的位置。而内存溢出
内存溢出,表示对象却是还存活着,导致GC 无法回收“有用的”对象,因此就需要检查 堆参数(-Xms 、-Xmx),与机器物理内存对比看是否还可以调大,或者从代码上检查,是否有对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
三、虚拟机栈溢出
在 HotSpot 虚拟机中是不区分 JVM 栈和本地方法栈的。设置本地方法栈大小的参数 -Xoss 实际上并无效果。
栈容量只由 -Xss 参数设定。
Java 虚拟机规范规定了两种栈异常:
1、如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出 StackOverflowError 异常。
2、如果虚拟机在扩展栈时无法申请到足够的内存空间,抛出 OutOfMemoryError 异常。
实际上,StackOverflowError 针对的是单独的虚拟机栈,而 OutOfMemoryError 则描述的是所有虚拟机栈。因为一个应用程序中很可能存在多个线程,因此这样区分异常可能是为了更精确的描述出现的问题。
相对来说,虚拟机栈内存出现问题多数都是 StackOverflowError,而且,Overflow的话会有错误堆栈可以阅读,相对比较好找到问题所在。而且,如果使用虚拟机默认参数,栈深度在大多数情况下,达到1000 到 2000 个栈帧完全没有问题。对于正常的方法调用和递归,这个深度应该完全够用。
因此,如果虚拟机栈发生 OOM ,很可能是由于栈深度过大,换句话说,-Xss 参数值过大。
案例演示
以下代码可能在Windows上运行会使系统假死,建议将重要文件和工作保存。
由于我的电脑本身执行上面的程序就会出现操作系统假死,因此这里贴出书中的执行结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
四、方法区和运行时常量池溢出
运行时常量池是方法区的一部分,因此,可以通过常量池溢出来测试这两个区域的溢出情况。
上面的代码通过 CGLib 动态生成大量的 Class 加载如 方法区。需要通过maven引入CGLib依赖:
<!-- https://mvnrepository.com/artifact/cglib/cglib --><dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.2.5</version></dependency>
但是代码执行后并未出现方法区内存溢出的问题。书中贴出的方法区内存溢出异常报错如下:
Caused by:java.lang.OutOfMemoryError : PermGen spaceat ...at ...at ...