深入解析 OOM 的三大场景
- 什么是 OOM?
- 一、堆内存溢出 ( Heap OOM )
- 原因分析
- 解决方案
- 二、栈内存溢出(Stack OOM)
- 原因分析
- 解决方案
- 三、方法区内存溢出(Metaspace OOM)
- 原因分析
- 解决方案
在Java应用程序开发中,OutOfMemoryError(OOM)是一个令人头痛的问题。当JVM中的内存无法满足应用程序的需求时,就会抛出这个错误。本文将深入探讨OOM的三大场景:堆内存溢出、方法区内存溢出和栈内存溢出,并分析它们的原因,提供相应的实战解决方案。
什么是 OOM?
OOM 的全称是 Out Of Memory,那我们的内存区域有哪些会发生 OOM 呢?我们可以从内存区域划分图上,看一下彩色部分
可以看到除了程序计数器,其他区域都有OOM溢出的可能。但是最常见的还是发生在堆内存溢出、方法区内存溢出和栈内存溢出,主要是在堆上。
另外,我们常说的 OOM 异常,其实是 Error
一、堆内存溢出 ( Heap OOM )
Java 堆用于存储对象实例,我们只要不断的创建对象,并且保证 GC Roots 到对象之间有可达路径来避免 GC 清除这些对象,那随着对象数量的增加,总容量触及堆的最大容量限制后就会产生内存溢出异常。
Java 堆内存的 OOM 异常是实际应用中最常见的内存溢出异常。
/*** JVM参数:-Xmx10m*/
public class JavaHeapSpaceDemo {static final int SIZE = 100 * 1024 * 1024;public static void main(String[] a) {int[] i = new int[SIZE];}
}
代码试图分配容量为 100M 的 int 数组,如果指定启动参数 -Xmx10m,分配内存就不够用,就类似于将 XXXL 号的对象,往 S 号的 Java heap space 里面塞。
原因分析
- 对象过多:应用程序创建了大量的对象,并且这些对象长时间存活,导致堆内存不足。
- 内存泄漏:应用程序中存在内存泄漏,即长时间无法释放不再使用的对象,导致堆内存持续占用。
- 超出预期的访问量/数据量:通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值
解决方案
针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可。如果仍然没有解决,可以参考以下情况做进一步处理:
- 优化代码:减少不必要的对象创建,避免过大的集合和数组。
- 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接
- 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。
二、栈内存溢出(Stack OOM)
栈内存溢出通常与线程的执行和递归调用有关。
public class StackOverflowErrorDemo {public static void main(String[] args) {javaKeeper();}private static void javaKeeper() {javaKeeper();}
}
原因分析
- 递归调用过深(最常见原因):递归算法实现不当,导致递归深度过大,超出了线程栈的大小限制。
- 线程创建过多:应用程序创建了大量的线程,并且每个线程的栈内存分配过多,导致系统资源耗尽。
解决方案
-
修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug
-
排查是否存在类之间的循环依赖
-
通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制
三、方法区内存溢出(Metaspace OOM)
方法区内存溢出通常与类的加载和元数据的存储有关。
JDK 1.8 之前会出现 Permgen space,该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。随着 1.8 中永久代的取消,就不会出现这种异常了。
Metaspace 是方法区在 HotSpot 中的实现,它与永久代最大的区别在于,元空间并不在虚拟机内存中而是使用本地内存,但是本地内存也有打满的时候,所以也会有异常。
/*** JVM Options: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m*/
public class MetaspaceOOMDemo {public static void main(String[] args) {while (true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(MetaspaceOOMDemo.class);enhancer.setUseCache(false);enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {//动态代理创建对象return methodProxy.invokeSuper(o, objects);});enhancer.create();}}
}
借助 Spring 的 GCLib 实现动态创建对象
Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace
方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景中,就应该特别关注这些类的回收情况。这类场景除了上边的 GCLib 字节码增强和动态语言外,常见的还有,大量 JSP 或动态产生 JSP 文件的应用(远古时代的传统软件行业可能会有)、基于 OSGi 的应用(即使同一个类文件,被不同的加载器加载也会视为不同的类)等。
原因分析
- 加载过多的类:每个类在加载到JVM时都会占用一定的方法区空间。如果程序加载了大量的类,那么方法区可能会被占满,导致OOM。
- 类加载器泄漏:如果类加载器没有正确地释放已经加载的类,那么这些类将一直占用方法区空间,导致方法区溢出。
- 动态生成类:在使用诸如JSP、反射或ASM等技术动态生成类时,如果生成过多的类或频繁地生成和卸载类,可能会导致方法区溢出
解决方案
- 限制方法区大小:通过-XX:MaxMetaspaceSize参数设置方法区的最大值,避免无限制增长。这需要根据应用程序的实际情况进行调整。
- 检查类加载器实现:确保自定义的类加载器正确实现了资源的释放,避免类加载器泄露。同时,注意检查和升级可能导致泄露的第三方库。
- 优化类加载策略:按需加载和卸载类,避免不必要的类加载。可以考虑使用模块化技术(如OSGi)来管理类的加载和卸载。