jvm性能调优实战 -58类加载器过多引发的OOM问题

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: jvm性能调优实战 -58类加载器过多引发的OOM问题

背景

公司里有一个非常正常的线上的服务,采用的是Web系统部署在Tomcat中的方式来进行启动的。

但是有一段时间,我们突然收到一些反馈,说是这个服务非常的不稳定,经常会出现访问这个服务的接口的时候出现服务的假死问题。

一旦出现这种接口调用时服务假死的情况,相当于我们的这个服务就完全不可用了,因此收到了不少上游服务的反馈。

但是上游服务反馈了另外一个非常关键的情况,就是经常一段时间内无法访问这个服务的接口,但是过了一会儿又可以访问了!

也就是说,似乎每次系统假死都只是一段时间而已!

因此我们着手进行了下面一系列的排查,最终解决了这个问题。


使用top命令检查机器资源使用

这里先给大家介绍一个技巧,因为当时的生产情况是服务假死,接口无法调用,并不是直接就抛出了OOM之类的异常。

因此其实你也很难直接去看他的线上日志,说根据日志立马就可以定位问题。

因此针对服务假死这个问题,我们首先可以先用linus的top命令去检查一下机器的资源使用量,通过这个命令可以看到机器上运行的各个进程对CPU和内存两种资源的使用量。

为什么要看这个呢?

因为如果服务出现无法调用接口假死的情况,首先要考虑的是两种问题。

  • 第一种问题:这个服务可能使用了大量的内存,内存始终无法释放,因此导致了频繁GC问题。

也许每秒都执行一次Full GC,结果每次都回收不了多少,最终导致系统因为频繁GC,频繁Stop the World,接口调用出现频繁假死的问题。

  • 第二种问题:可能是这台机器的CPU负载太高了,也许是某个进程耗尽了CPU资源,导致你这个服务的线程始终无法得到CPU资源去执行,也就无法响应接口调用的请求。这也是一种情况。

因此针对服务假死的问题,通过top命令先看一下,立马心里就有数了。

针对线上这台机器使用top命令检查之后,就发现了一个问题,这个服务的进程对CPU耗费很少,仅仅耗费了1%的CPU资源,但是他耗费了50%以上的内存资源。这个就引起了我们的注意。

因为这台机器是4核8G的标准线上虚拟机,针对这种机器通常我们会给部署的服务的JVM总内存在5G6G,刨除掉Metaspace区域之类的,堆内存大概会给到4G5G的样子,毕竟还得给创建大量的线程留下一部分的内存。

之前给大家介绍过,JVM使用的内存主要是三类,栈内存、堆内存和Metaspace区域,现在一般会给Metaspace区域512MB以上的空间,堆内存假设有4G,然后栈内存呢?每个线程一般给1MB的内存,那么如果你JVM进程中有几百上千个线程,也会有将近1G的内存消耗。

此时JVM进程其实耗费的总内存就接近6G了,另外你还得给操作系统内核以及其他的进程留出一部分的内存空间去使用。

因此最终让你的JVM可以使用的堆内存大概也就是机器上一半的内存而已。

大家到这里就知道了,为什么我们看到这个服务进程对内存耗费超过50%感到有点惊讶,因为这说明他几乎快要把分配给他的内存消耗殆尽了!

而且最关键的是,他长期保持对内存资源的消耗在50%以上,甚至达到更高,说明他GC的时候并没有把内存回收掉!


在内存使用这么高的情况下会发生什么?

此时我们开始思考一个问题,既然这个服务的进程对内存使用率这么高,可能发生的问题也就三种。

  • 第一种是内存使用率居高不下,导致频繁的进行full gc,gc带来的stop the world问题影响了服务。
  • 第二种是内存使用率过多,导致JVM自己发生OOM。
  • 第三种是内存使用率过高,也许有的时候会导致这个进程因为申请内存不足,直接被操作系统把这个进程给杀掉了!

所以此时我们就开始分析,到底是哪种问题呢?

首先我们先使用jstat分析了一下JVM运行的情况,确实内存使用率很高,也确实经常发生gc,但是实际上gc耗时每次也就几百毫秒,并没有耗费过多的时间

也就是说虽然gc频率高,但是其实是个常规现象。

而且我们发现这个服务经常频繁gc,但是有的时候频繁gc的时候,也没听见上游服务反馈说服务假死!因此第一种情况其实直接可以过滤掉了。

接着我们分析第二种情况,难道是JVM自己有时候发生OOM挂掉了?挂掉的时候必然导致服务无法访问,上游服务肯定会反馈说我们服务死掉的!

但是我们检查了一下应用服务自身的日志,并没有看到任何日志输出了OOM异常!

接着我们猜测,也许是第三种问题,就是JVM运行的时候要申请的内存过多,结果内存不足了,有时候os会直接杀掉这个进程!

可能在进程被杀掉的过程中,就出现了上游服务无法访问的情况,但是我们的进程都是有监控脚本的,一旦进程被杀掉,会有脚本自动把进程重新启动拉起来。

所以也许其他服务过一会就会发现,服务又可以访问了!


底是谁占用了过多的内存?

既然如此,按照我们的思路继续分析下去,如果要解决这个问题,就必须要找出来,到底是什么对象占用我们的内存过多,进而申请过多的内存,最后导致进程被杀掉了?

很简单,直接从线上导出一份内存快照即可。

我们在线上系统运行一段时间过后,用top命令和jstat命令观察了一段时间,发现jvm已经耗费了超过50%的内存了,此时迅速导出了一份内存快照进行分析。

此时用MAT进行内存快照分析的时候,我们发现,居然是一大堆的ClassLoader也就是类加载器,有几千个,而且这些类加载器加载了的东西,都是大量的byte[]数组,所有这些一共占用了超过50%的内存。

看起来元凶就是他了!

那这些ClassLoader是哪儿来的?为什么会加载那么多的byte[]数组?

具体原因就不说了,但是大家应该知道一点,我们除了用类加载加载类以外,其实还可以用类加载器去加载一些其他的资源,比如说一些外部配置文件什么的。

当时写这个系统代码的工程师做了自定义类加载器,而且在代码里没有限制的创建了大量的自定义类加载器,去重复加载了大量的数据,结果经常一下子就把内存耗尽了,进程就被杀掉了!

因此解决这个问题非常的简单,直接就是修改代码,避免重复创建几千个自定义类加载器,避免重复加载大量的数据到内存里来,就可以了。


小结

其实所谓的案例实战,他们背后的原理都是一套东西,归根结底从原理层面都是类似的

但是用不同的案例,可以告诉大家各种不同的故障场景以及不同问题的分析思路,这个是案例对大家最有用的一个地方。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
13天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
20天前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
25 1
|
22天前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
23天前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
39 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
1月前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
2月前
|
Arthas 监控 数据可视化
JVM进阶调优系列(7)JVM调优监控必备命令、工具集合|实用干货
本文介绍了JVM调优监控命令及其应用,包括JDK自带工具如jps、jinfo、jstat、jstack、jmap、jhat等,以及第三方工具如Arthas、GCeasy、MAT、GCViewer等。通过这些工具,可以有效监控和优化JVM性能,解决内存泄漏、线程死锁等问题,提高系统稳定性。文章还提供了详细的命令示例和应用场景,帮助读者更好地理解和使用这些工具。
|
2月前
|
监控 架构师 Java
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。
|
2月前
|
Java
JVM进阶调优系列(5)CMS回收器通俗演义一文讲透FullGC
本文介绍了JVM中CMS垃圾回收器对Full GC的优化,包括Stop the world的影响、Full GC触发条件、GC过程的四个阶段(初始标记、并发标记、重新标记、并发清理)及并发清理期间的Concurrent mode failure处理,并简述了GC roots的概念及其在GC中的作用。