OOM 即 out of memory(内存溢出),当程序运行时所需要的内存超出了JVM管理的最大内存就会导致内存溢出。
经常与内存溢出一起被提及的概念还有内存泄漏(memory leak),内存泄漏一般是指资源应释放而未释放,内存应回收而未回收,造成系统资源或内存浪费。当内存泄漏累计之后,资源和内存会被逐渐耗光,导致程序运行越来越慢,可能会导致OOM,甚至程序奔溃。
当内存溢出发生时,会抛出OutOfMemoryError
或者StackOverflowError
异常。但是当内存泄漏发生时,若还有资源和内存可用,则不会出现异常。
㈠ JVM堆内存溢出
堆内存溢出时抛出的异常是 java.lang.OutOfMemoryError:Java heap space
。对象基本都是在堆中分配内存,所以导致堆内存溢出的原因极有可能与对象有关。
⑴ 可能导致堆内存溢出的原因
① 堆内存分配得不合理。堆内存配置的太小,或者新生代与老年代的比例配置得不合理,导致创建对象时没有内存空间可以分配。
② 存在大对象。例如大数组、大字符串、Map、集合等。
③ 大量的静态属性,用作本地缓存的Map、List等。
④ 频繁创建对象。例如在循环中不停的创建对象。
⑤ 一次从数据库中读取大量的数据。
⑥ 数据/流量峰值,程序没有做限流,数据量突然激增,导致内存不足。
⑦ 存在内存泄漏。导致内存泄漏的原因主要可以分为两类,一类是资源未释放,例如IO、数据库连接等;另一类是长生命周期的对象引用了短生命周期的对象,导致短生命周期的对象无法被回收,例如经常被提及的ThreadLocal内存泄漏问题。
堆内存溢出代码示例:
public class HeapOomTest {
private static final Logger LOGGER = LoggerFactory.getLogger(HeapOomTest.class);
private static List<byte[]> CACHE = new ArrayList<>(1000);
public static void main(String[] args) {
for (int loopTimes = 1; loopTimes < 20; loopTimes++) {
long cacheSize = ObjectSizeCalculator.getObjectSize(CACHE);
LOGGER.info("第 {} 次循环前, 对象已占用 {} M 内存", loopTimes, cacheSize / 1024 / 1024);
// 每次占用5M内存
CACHE.add(new byte[5 * 1024 * 1024]);
}
}
}
使用IDEA启动的时候,设置JVM的堆内存最大值为64M,如下图:
执行结果:
在第10次循环时,CACHE对象已占用了45M内存,再次创建byte数组申请5M内存时抛出OOM异常,可以看出,这个程序运行时,占用了近20M的堆内存。
⑵ 堆内存溢出的问题定位
定位问题,首先要找到java进程的ID(PID),使用 jps -vl
命令即可找到java进程的PID和启动时设置的jvm参数。
① 针对第一种场景,可以使用 jinfo PID
命令查询jvm的配置参数,检查内存配置是否合理。
如果确实是内存配置的太小了,则可以调整-Xms
和-Xmx
参数,然后通过压力测试将这两个参数调整到最优值。
② 如果是其它场景,即对象占用了大量的内存,导致无法为新对象分配内存。此时需要使用 jmap 和 MAT 等工具进行分析。主要是要找出占用内存最多的对象。
直接使用 jmap -histo:live PID
命令查看哪个类型的对象占用了大量的内存。
或者使用jmap -dump:live,format=b,file=jvm_heapLive.hprof PID
命令将堆中的对象信息导出,然后使用MAT等工具进行分析。
MAT工具分析dump文件:
找到了占用内存多的对象后,就可以查看代码,确定问题了。
在JVM启动的时候可以添加下面两个参数,开启发生OOM时,会自成生成dump文件:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/dump
编程时建议不要使用大对象,例如读取文件时,不要一次性读取完成再处理,可以读一部分处理一部分;处理数据库中的大量数据时,分批进行处理。
使用完的资源及时释放,尽可能的不要使用Map、List等静态属性做本地缓存。
做好限流、消峰,避免突增的流量和数据造成程序奔溃。
㈡ 元空间内存溢出
元空间和永久代都是JVM规范中方法区的实现,当内存不足时,也会抛出java.lang.OutOfMemoryError: Metaspace/PremGen space
异常。永久代是JDK1.7及以前的内存区域,从JDK1.8开始用元空间替换了永久代,所以主要分析看一下元空间内存溢出的问题。元空间主要存储的是类型信息(class)和JIT编译后的代码,所以元空间的内存溢出主要与加载的class有关。
可能导致元空间内存溢出的原因及解决方法:
① 元空间的内存分配的太小。这种情况应该是见得最多的,处理方式与堆内存一样,调整适当的调整 -XX:MetaspaceSize
和 -XX:MaxMetaspaceSize
两个参数。
② 加载了太多太多的类,例如引入了大量的第三方jar包,程序本身也有非常多的类等等。当出现这种情况,应尽量减少依赖的jar包,将单个服务拆分多个。
③ 程序运行时动态的生成class,例如CGLIB动态生成class。所有当有动态生成class时,要做好压力测试,将元空间内存设置在一个适合的范围。
④ 还想到一种情况没有验证过,就是自定义的类加载器比较多,每个类加载器都重复加载相同的类。
CGLIB动态生成class导致OOM的示例代码:
public class MetaspaceOomTest {
private static final Logger LOGGER = LoggerFactory.getLogger(MetaspaceOomTest.class);
public static void main(String[] args) {
ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaspaceOomTest.class);
enhancer.setCallbackTypes(new Class[]{MethodInterceptor.class, InvocationHandler.class});
enhancer.setCallbackFilter(new CallbackFilter() {
@Override
public int accept(Method method) {
return 0;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
});
Class clazz = enhancer.createClass();
LOGGER.info("CGLIB class : {}, TotalLoadedClassCount : {}", clazz.getName(), classLoadingMXBean.getTotalLoadedClassCount());
}
}
}
将元空间设置得比较小,方便快速出现OOM:
执行结果:
㈢ 栈内存溢出
每个方法在执行时都会生成一个栈帧,栈帧中保存了当前操作数、局部变量、返回地址等等一些信息,栈帧都会被纳入栈。当一个线程执行的方法太多就会出现栈内存溢出 java.lang.StackOverflowError
,例如递归调用。可以通过参数-Xss适当的调整栈的大小。
栈内存溢出的示例代码:
public class StackOverflowTest {
private static final Logger LOGGER = LoggerFactory.getLogger(StackOverflowTest.class);
public static void main(String[] args) {
StackOverflowTest stackOverflowTest = new StackOverflowTest();
LongAdder longAdder = new LongAdder();
stackOverflowTest.callMethod(longAdder);
}
public void callMethod(LongAdder longAdder) {
longAdder.increment();
LOGGER.info("第 {} 次调用", longAdder.longValue());
this.callMethod(longAdder);
}
}
将桟内存设置的足够小:
执行结果:
OOM的场景还有很多,例如:
直接内存溢出,java.lang.OutOfMemoryError: Direct buffer memory
GC回收超时引发的内存溢出,java.lang.OutOfMemoryError: GC overhead limit exceeded
压缩类空间内存溢出,java.lang.OutOfMemoryError: Compressed class space
。。。。。。