Jvm中各种内存溢出情况分析

  • 2020 年 1 月 21 日
  • 笔记

本文以JDK8来研究讨论,其它JDK可能有不同的结果。

oom即OutOfMemoryError,出现这个报错的主要原因是内存空间不足以装下数据导致抛出异常。要探讨JVM出现oom的情况,首先要了解下jvm的内存模型。

上图中每个区域都可能出现oom,除此之外还有直接内存(direct memory)溢出。

堆溢出

java堆用于存储对象实例,只要不断地产生对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。

可达性分析算法

判断对象是否可以回收采用的是可达性分析算法,只要被gc roots引用的对象就不会被回收。那么gc root有那几种?一个对象可以属于多个root,GC root有几下种: • Class – 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots

• Thread – 活着的线程

• Stack Local – Java方法的local变量或参数

• JNI Local – JNI方法的local变量或参数

• JNI Global – 全局JNI引用

• Monitor Used – 用于同步的监控对象

• Held by JVM – 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此就只有留给分析分员去确定哪些是属于"JVM持有"的了。

例子

import java.util.ArrayList;  import java.util.List;    /**   * VM args: -Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath={HeapDump文件目录}   *   * @author donghaibin   * @date 2020/1/20   */  public class HeapOomTest {        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_pid56168.hprof ...  Heap dump file created [28216756 bytes in 0.077 secs]  Exception in thread "main" java.lang.OutOfMemoryError: Java heap space      at java.util.Arrays.copyOf(Arrays.java:3210)      at java.util.Arrays.copyOf(Arrays.java:3181)      at java.util.ArrayList.grow(ArrayList.java:265)      at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)      at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)      at java.util.ArrayList.add(ArrayList.java:462)      at jvm.HeapOomTest.main(HeapOomTest.java:21)    Process finished with exit code 1

通过mat内存分析工具打开dump出来的文件,如果是内存泄漏,查看泄漏对象到gc roots的引用链,找到泄漏对象是通过怎样的路径与gc roots相关联并导致垃圾收集器无法自动回收它们的,就能比较准确定位出泄漏代码的位置。如果不是内存泄漏,换句话说,就是堆里的内存必须存活,那就考虑增大堆的大小、代码上检查是否有对象生命周期过长,尝试减少程序运行期的内存消耗。

虚拟机栈与本地方法栈溢出

Hotshot不区分虚拟机栈和本地方法栈,因此,通过-Xoss参数设置本地方法栈的大小实际上是无效的。栈容量只能通过-Xss参数设定。关于虚拟机栈和本地方法栈的溢出,在Java虚拟机规范中描述了两种异常:

  • 线程执行深度大于虚拟机所允许的深度时,将抛出StackOverflowError
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError

运行一个线程就会创建一个虚拟机栈,每个方法的调用对应栈中的栈帧

StackOverflowError例子

递归执行stackLeek方法,每次向栈中压入一个栈帧,当大于虚拟机所需要的允许时就抛出异常

/**   * Vm args: -Xss128k   *   * @author donghaibin   * @date 2020/1/20   */  public class StackOomTest {        private static int stackLength = 1;        public void stackLeek() {          stackLength++;          stackLeek();      }        public static void main(String[] args) {          StackOomTest stackOomTest = new StackOomTest();          try {              stackOomTest.stackLeek();          } catch (Throwable throwable) {              System.out.println("stack length: " + stackLength);              throw throwable;          }      }    }

运行结果:

stack length: 1885  Exception in thread "main" java.lang.StackOverflowError      at jvm.StackOomTest.stackLeek(StackOomTest.java:15)      at jvm.StackOomTest.stackLeek(StackOomTest.java:15)      ...      at jvm.StackOomTest.stackLeek(StackOomTest.java:15)

OutOfMemoryError例子

操作系统为每个进程分配内存是有限制的,譬如32位的Windows限制为2G。虚拟机提供参数控制堆和方法区这两部分内存大小,剩下的内存由虚拟机栈和本地方法栈瓜分分配给进程的总内存减去最大堆内存减去方法区,程序计数器占用的内存小,可以忽略,剩下的就是虚拟机栈和本地方法栈的内存大小。