首先,我们说OOM,也就是我们的Java代码可能会导致的计算机各个部分的内存溢出。那么,都有哪些位置容易出现OOM呢?废话少说,我们直接上图。

概览运行时数据区

lua 递归 递归oom_JVM

可以看到,对于我们的Java程序来说,一共就这么多片区域。那么哪些区域会导致内存溢出呢?

栈内存溢出

HotSpot虚拟机中栈的大小是固定的,不支持扩展,但是默认大小都是1M。虽然不支持扩展,但是可以在启动的时候用-Xmx参数设置每一个虚拟机栈的默认值,再启动。

既然栈的大小是固定的,那就有OOM的可能。但是一般我们在开发中,普通方法之间的调用是很难出现OOM的,那么导致栈区OOM的常见原因会是什么呢?

无限递归导致栈OOM

Java.lang.StackOverflowError(单个虚拟机栈)一般普通的方法调用是很难出现的,如果遇到了可能写了无限递归。

虚拟机栈带给我们的启示:方法的执行因为要打包成栈帧。所以天生比面向过程的简单循环要慢。所以树的遍历算法中递归和非递归(这也就是为什么虚拟机优化要朝着方法内联而努力的原因)都有其存在的意义。递归的代码更简洁,非递归代码复制但速度快。

栈内存的OOM(此处指的整个运行时数据区的栈空间)的发生条件是:不断地创建栈帧(方法),就会不断地消耗其所在的虚拟机栈空间。而我们的虚拟机栈满打满算默认也就1M,因此就会造成OOM啦。

下面来看一个简单的死递归案例:

lua 递归 递归oom_lua 递归_02

lua 递归 递归oom_JVM_03

堆内存溢出(超级重点,调优核心)

内存溢出

申请内存空间超出最大堆内存空间。

如果并非是程序问题,而是我们程序确实大量内存而JVM无法满足我们时,则通过调整堆内存参数解决。

内存泄漏

指长期不使用,却一直无法回收的对象。

如果是内存泄漏,但堆中存储的对象又必须是存活的(使用率极低),那么就应该检查JVM的堆参数设置,与机器内存相比,哪些空间是可调的。再从代码上检查是否存在某些对象生命周期过长,存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。

堆内存OOM如何解决

堆内存OOM产生的情况非常多。如果是程序正常的情况下导致的OOM,那么一般可以调大-Xms参数来设置堆的大小,或是直接充钱换机器。如果是内存泄漏,那么就需要排查自己的代码。关键咱们要在之后的学习中,学会定位OOM的原因以及具体位置,再做出相应的处理。

方法区溢出

运行时常量池溢出

常量池中保存的都是各种各样的常量。当常量过多时可能会导致内存溢出。

在JDK6之前的永久代中,常量池会出现溢出情况。随着永久代更替为元空间后,我们的方法区常量池移动到了堆内存。因此常量池溢出的情况已经很少见了

Class对象或Class信息过多导致溢出

java永久代(元数据)溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize/MetaspaceSize=64m      -XX:MaxPermSize/MaxMetaspaceSize =256m的形式修改

补充:Class回收的条件

回收class的条件非常苛刻。

1,该类的所有实例都已经被回收,也就是堆内存中不再存在任何该类的实例

2,记载该类的ClassLoader已经被回收

3,该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。

也就是说,一般如果真的方法区不够用了,那么最好调整机器或是调大参数来解决问题。

本机直接内存溢出

直接内存的零拷贝

直接内存不是JAVA虚拟机规范中定义的内存区域。在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,可以使用native函数库直接分配堆外内存(本地方法栈干的事),然后通过抑制存储在JAVA堆中的DirectByteBuffer对象对这块内存的引用进行操作。这样就能在一些场景中显著提升性能,避免了Java堆和堆外内存的Native堆中来回复制数据。

1. 本机直接内存的分配不会受到Java堆大小的限制,收本机总内存大小的限制。

2. 直接内存的大小可以用参数设置

3. 直接内存申请空间耗费更高的性能

4. 直接内存IO读写的性能要优于普通内存

当我们需要频繁访问大的内存而不是申请和释放空间的时候,通过使用直接内存可以提高性能,也就是常说的零拷贝

无限创建线程导致OOM

直接内存的OOM可能的发生条件是:不断地创建线程,JVM会向操作系统不断地申请直接内存,机器没有足够的内存,就会导致直接死机。

当你使用JAVA线程,JVM内会创建一个Thread对象。但是同时也会在操作系统里创建一个真正的物理线程(参考JVM规范),操作系统会在剩下的内存中创建这个物理线程,而不是在JVM中。因此想要更多的创建线程,不但要消耗在其运行期间在JVM运行时数据区的空间,还要预留充足的堆外内存。

lua 递归 递归oom_内存溢出_04

lua 递归 递归oom_lua 递归_05

操作Unsafe类

Unsafe类是Java中操作直接内存的类,但JDK1.9以后不用了。

使用UnSafe类可以不断地对直接内存申请空间,最终会OOM。不仅如此,有一些类的底层依旧会使用Unsafe方法操作对堆外内存,使用时可以多多留意。

难以排查的特性

由于申请直接内存不由虚拟机管理,所以由此导致的OOM是不会在Heap Dump文件中看出明显异常。当OOM后发现Dump文件很小同时程序直接或间接的使用了NIO,可以考虑是否是直接内存溢出。