jvm性能调优实战 - 46堆区OOM解析

简介: jvm性能调优实战 - 46堆区OOM解析

Pre

之前的文章已经分析了Metaspace和栈内存两块内存区域发生内存溢出的原理,同时给出了一些较为常见的引发他们内存溢出的场景,一般只要代码上注意一些,不太容易引发那两块区域的内存溢出。

重点要来了,真正最容易引发内存溢出的,说白了就是平时我们系统创建出来的对象实在是太多了,最终就导致了系统的内存溢出!


从对象在Eden区分配开始讲起

如果要把这大量的对象是如何导致堆内存溢出的给讲清楚,那就得从系统运行,在Eden区创建对象开始讲起了。

咱们都知道,平时系统运行的时候一直不停的创建对象,然后大量的对象会填满Eden区

一旦Eden区满之后,就会触发一次Young GC,然后存活对象进入S区。

如下图所示


高并发场景下导致ygc后存活对象太多

当然因为各种各样的情况,一旦出现了高并发场景,导致ygc后很多请求还没处理完毕,存活对象太多,可能就在Survivor区域放不下了,此时就只能进入到老年代里去了,老年代很快就会放满了,如下图所示。

一旦老年代放满了就会触发Full GC,如下图所示。

我们假设ygc过后有一批存活对象,Survivor放不了,此时就等着要进入老年代里,然后老年代也满了,那么就得等着老年代进行CMS GC,必须回收掉一批对象,才能让年轻代里存活下来的一批对象进去。

但是呢,不幸的事情发生了,老年代GC过后,依然存活下来了很多的对象!如下图所示。

这个时候如果年轻代还有一批对象等着放进老年代,人家GC过后空间还是不足怎么办?

还能怎么办!只能是内存溢出了!如下图所示!

所以这个时候,老年代都已经塞满了,你还要往里面放东西,而且触发了Full GC回收了老年代还是没有足够内存空间,你坚持要放?那只能给你一个内存溢出的异常了!JVM跑不动了,崩溃掉。

这个就是典型的堆内存实在放不下过多对象的内存溢出的一个典型范例。


什么时候会发生堆内存的溢出?

发生堆内存溢出的原因其实总结下来,就一句话:

有限的内存中放了过多的对象,而且大多数都是存活的,此时即使GC过后还是大部分都存活,所以要继续放入更多对象已经不可能了,此时只能引发内存溢出问题。

所以一般来说发生内存溢出有两种主要的场景:

  • 系统承载高并发请求,因为请求量过大,导致大量对象都是存活的,所以要继续放入新的对象实在是不行了,此时就会引发OOM系统崩溃
  • 系统有内存泄漏的问题,就是莫名其妙弄了很多的对象,结果对象都是存活的,没有及时取消对他们的引用,导致触发GC还是无法回收,此时只能引发内存溢出,因为内存实在放不下更多对象了

因此总结起来,一般引发OOM,要不然是系统负载过高,要不然就是有内存泄漏的问题

这个OOM问题,一旦你的代码写的不太好,或者设计有缺陷,还是比较容易引发的,所以这个问题也是我们后面要重点分析的。


Case Demo

一旦要是系统负载过高,比如并发量过大,或者是数据量过大,或者是出现了内存泄漏的情况,很容易就导致JVM内存不够用了,就会堆内存溢出,然后系统崩溃。 所以今天就体验一下堆内存溢出的场景。

Review 堆内存溢出的一个典型场景

首先还是来一张完整的JVM运行原理图,里面包含了对象的分配,GC的触发,对象的转移,各个环节如何触发内存溢出的,大家一定要牢记这张图。

接着我们就来回顾一下一个典型的堆内存溢出的场景:

假设现在系统负载很高,不停的运转和工作,不停的创建对象塞入内存里,刚开始是塞入哪里的?

当然是年轻代的Eden区了,如下图红圈所示。

但是因为系统负载实在太高了,很快就把Eden区塞满了,这个时候触发ygc

但是ygc的时候发现不对劲,因为似乎Eden区里还有很多的对象都是存活的,而且survivor区域根本放不下,这个时候只能把存活下来的大批对象放入老年代中去,如下图红圈处。

就这么来几次ygc之后,每次ygc后都有大批对象进入老年代,老年代很快就会塞满了,而且最重要的是这里的对象还大多都是存活的。

所以接下来一次ygc后又要转移一大批对象进入老年代,先触发full gc,但是full gc之后老年代里还是塞满了对象,如下图红圈所示。

这个时候ygc后存活下来的对象哪怕在full gc之后还是无法放入老年代中,此时就直接报出堆内存溢出了,如下图红圈所示。

所以堆内存溢出的场景就是这样子,非常的简单 。


模拟Code

代码很简单,就是在一个while循环里不停的创建对象,而且对象全部都是放在List里面被引用的,也就是不能被回收。

大家试想一下,如果你不停的创建对象,Eden区满了,他们全部存活会全部转移到老年代,反复几次之后老年代满了。

然后Eden区再次满了,ygc后存活对象再次进入老年代,此时老年代先full gc,但是回收不了任何对象,因此ygc后的存活对象就一定是无法进入老年代的。

所以我们用下面的JVM参数来运行一下代码:-Xms10m -Xmx10m,我们限制了堆内存大小总共就只有10m,这样可以尽快触发堆内存的溢出。

我们在控制台打印的信息中可以看到如下的信息:

当前创建了第360145个对象

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

所以从这里就可以看到,在10M的堆内存中,用最简单的Object对象搞到老年代被塞满大概需要36万个对象。然后堆内存实在放不下任何其他对象,此时就会OutOfMemory了,而且告诉了你是Java heap space,也就是堆空间发生了内存溢出的。


小结

可以看到堆内存是如何溢出的,以及溢出的时候回看到什么样的异常信息。


相关文章
|
5天前
|
canal 缓存 关系型数据库
Spring Boot整合canal实现数据一致性解决方案解析-部署+实战
Spring Boot整合canal实现数据一致性解决方案解析-部署+实战
|
5天前
|
C++
C++:深度解析与实战应用
C++:深度解析与实战应用
8 1
|
5天前
|
大数据 图形学 云计算
EDA设计:技术深度解析与实战代码应用
EDA设计:技术深度解析与实战代码应用
|
12天前
|
缓存 监控 Java
深入理解Java虚拟机(JVM)性能调优
【4月更文挑战第18天】本文探讨了Java虚拟机(JVM)的性能调优,包括使用`jstat`、`jmap`等工具监控CPU、内存和GC活动,选择适合的垃圾回收器(如Serial、Parallel、CMS、G1),调整堆大小和新生代/老年代比例,以及代码优化和JIT编译策略。通过这些方法,开发者能有效提升应用性能并应对复杂性挑战。性能调优是持续过程,需伴随应用演进和环境变化进行监控与优化。
|
13天前
|
监控 Java 调度
探秘Java虚拟机(JVM)性能调优:技术要点与实战策略
【4月更文挑战第17天】本文探讨了JVM性能调优的关键技术,包括内存模型调优(关注堆内存和垃圾回收),选择和优化垃圾收集器,利用JVM诊断工具进行问题定位,以及实战调优案例。强调了开发者应理解JVM原理,善用工具,结合业务场景进行调优,以应对高并发和大数据量的挑战。调优是持续的过程,能提升系统稳定性和效率。
|
14天前
|
监控 前端开发 安全
JVM工作原理与实战(十四):JDK9及之后的类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JDK8及之前的类加载器、JDK9及之后的类加载器等内容。
19 2
|
14天前
|
监控 Java 关系型数据库
JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。
15 2
|
14天前
|
监控 前端开发 安全
JVM工作原理与实战(十一):双亲委派机制
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了双亲委派机制、父类加载器、双亲委派机制的主要作用、双亲委派机制常见问题等内容。
14 1
|
14天前
|
监控 安全 Java
JVM工作原理与实战(九):类加载器-启动类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了启动类加载器、通过启动类加载器去加载用户jar包等内容。
28 8
|
14天前
|
Arthas 安全 Java
JVM工作原理与实战(八):类加载器的分类
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了类加载器、类加载器的分类等内容。
18 4

推荐镜像

更多