OOM如何解决

为什么会OOM

在《Java 虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError(下文称 OOM)异常的可能。表示内存耗尽。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个错误

为什么会出现 OOM,一般由这些问题引起

  • 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理
  • 代码漏洞:某一个对象被频繁申请,不用了之后却没有被释放,导致内存耗尽

内存泄漏:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。因为申请者不用了,而又不能被虚拟机分配给别人用

内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出

内存泄漏持续存在,最后一定会溢出,两者是因果关系

java堆溢出

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

​ Java 堆内存的 OutOfMemoryError 异常是实际应用中最常见的内存溢出异常情况。出现 Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”。

  1. 要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对 Dump 出来的堆转储快照进行分析。第一步首先应确认内存中导致 OOM 的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链,找到泄漏对象是通过怎样的引用路径、与哪些 GC Roots 相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到 GC Roots 引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
  3. 如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查 Java 虚拟机的堆参数(-Xmx 与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

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

由于 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因此对于 HotSpot 来说,-Xoss 参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss 参数来设定。关于虚拟机栈和本地方法栈,在《Java 虚拟机规范》中描述了两种异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError 异常。

  2. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。

《Java 虚拟机规范》明确允许 Java 虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot 虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OutOfMemoryError 异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致 StackOverflowError 异常。

image-20231126145507806

方法区和运行时常量池溢出

​ 方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,要达成的条件是比较苛刻的。在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。这类场景除了之前提到的程序使用了 CGLib 字节码增强和动态语言外,常见的还有:大量 JSP 或动态产生 JSP 文件的应用(JSP 第一次运行时需要编译为Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

​ 在 JDK 8 以后,永久代便完全退出了历史舞台,元空间作为其替代者登场。在默认设置下,前面列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常了。不过为了让使用者有预防实际应用里出现类似于代码清单 2-9 那样的破坏性的操作,HotSpot 还是提供了一些参数作为元空间的防御措施,主要包括:

  • ·-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
  • ·-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
  • ·-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:MaxMetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。

本机直接内存溢出

​ 直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。但是,这部分内存也被频繁地使用,而且也可能导致OOM。

​ 在JDK1.4 中新加入了NIO(New Input/Output)类,它可以使用 native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

  • 本机直接内存的分配虽然不会受到Java 堆大小的限制,但是受到本机总内存大小限制。
  • 直接内存由 -XX:MaxDirectMemorySize 指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。
  • NIO程序中,使用ByteBuffer.allocteDirect(capability)分配的是直接内存,可能导致直接内存溢出。

直接内存溢出

  • 检查代码是否恰当
  • 检查JVM参数-Xmx,-XX:MaxDirectMemorySize 是否合理。

注:以上内容大多出自周志明老师的《深入理解java虚拟机:第三版》