什么是内存溢出?在哪些区域会发生内存溢出?回答这个问题,我们需要先看一看Java代码是怎么运行的。现在计入说我们写了一个 HelloWorld.class
:
|
|
我们来分析一下这段代码的运行情况:
- 源文件
HelloWorld.java
将会被编译成可执行文件HelloWorld.class
- 类加载加载可执行文件到 Metaspace,Metaspace 保存类的基本信息,如果加载太多就会 OOM
- Java是多线程的,运行代码的时候会启动一个线程。
main()
是Java程序的入口,首先会启动一个 main 线程。每个线程都有 Java 虚拟机栈,每执行一个方法都会有一个栈帧入栈,栈帧中包含参数、局部变量、返回值地址等信息。如果代码层次太深,不断有方法入栈却没有出栈,Java虚拟机栈就会 OOM。 - 栈中的局部变量如果是一个对象,那就会在初始化的时候在堆中创建对象。堆中创建的对象过多就会触发 GC,GC 的速度赶不上新建对象的速度也会发生 OOM。
从 Java 代码的运行过程来看,有三个区域会发生 OOM,它们分别是:Metaspace、Java 虚拟机栈、堆内存。
Metaspace 是如何溢出的?
Metaspace 中会加载类,保存各种类信息,它的大小可以通过 -XX:MetaspaceSize=512m
和 -XX:MaxMetaspaceSize=512m
来设置。
当我们不断地创建类,不断地创建类,把 Metaspace 的内存都给占满了。这个时候就会触发 Full GC,Full GC 一般会顺带着进行 Minor GC 回收年轻带,也会进行 Old GC 回收老年代。
如果 Full GC 回收 Metespace 中的空间之后,任然无法存放新建的对象,此时 OOM 就会发生了。
不过 Metaspace 一般很少发生 OOM,如果发生了一般是出于下面两个原因:
- 上线系统的时候对 Metaspace 区域直接用默认的参数,即根本不设置其大小。默认的Metaspace区域可能才几十MB,对于稍微大型的系统,他自己有很多类,还依赖了很多外部的jar包,几十MB的Metaspace很容易就不够了。
- 写代码的时候用到了类似与 cglib 的动态代理技术,代码写得不好就会导致创建太多的类。
线程中的栈是如何溢出的?
虚拟机中的栈内存也是有限的,我们调用方法的时候会创建一个栈帧,紧接着方法入栈。如果一个线程一直调用方法入栈,栈内存终归是要满的,此时线程的栈中就会发生 OOM。
发生这种情况一般就是代码除了问题,比如写了个递归调用,和 Metaspace 的内存溢出一样,也很少发生。
堆内存是如何溢出的?
最常见的就是堆内存溢出的问题了。堆内存有限的情况下,不断创建对象并触发 GC,但是 GC 过后任然有大量大象存活,此时还要放入大量新的对象,这就会导致堆内存溢出问题。
一般来说对内存溢出有这样两个场景:
- 高并发场景下,请求量太大,创建了大量新的对象,且这些都是有用的、存活的。堆中无法放入更多对象就会导致堆内存溢出
- 内存泄漏问题,长生命周期的对象引用了大量短生命周期的对象,没有及时取消对它们的引用,导致 GC 无法回收这些理应被回收的对象,就导致了堆内存溢出