JVM性能调优——OOM分类及解决方案

news2024/11/24 4:35:36

文章目录

  • 1、概述
  • 2、OOM案例1:堆内存溢出
  • 3、OOM案例2:元空间溢出
  • 4、OOM案例3:GC overhead limit exceeded
  • 5、OOM案例4:线程溢出
  • 6、小结

在工作中会经常遇到内存溢出(Out Of Memory,OOM)异常的情况,每当遇到OOM,总是让人头疼不已,不知如何下手解决。本帖汇总了OOM产生的不同场景,从案例出发,模拟产生不同类型的OOM,针对不同类型的OOM给出相应的解决方案。

1、概述

当JVM没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出OOM异常。OOM可以分为四类,分别是堆内存溢出、元空间溢出、GC overhead limit exceeded和线程溢出。

2、OOM案例1:堆内存溢出

模拟线上环境产生OOM,代码清单如下所示:
在这里插入图片描述
JVM参数配置如下:

在这里插入图片描述
运行结果如下所示:
在这里插入图片描述
运行程序得到heapdump.hprof文件,在设置的heap目录下,如下图所示:
在这里插入图片描述
由于我们当前设置的内存比较小,所以该文件比较小,但是正常在线上环境,该文件是比较大的,通常以G为单位。

下面使用工具分析堆内存文件heapdump.hprof,通过Java VisualVM工具查看哪个类的实例占用内存最多,这样就可以初步定位到问题所在。如下图所示,可以看到在堆内存中存在大量的People类对象,占用了99.9%内存,基本上就可以定位问题所在了。当然这里的代码比较简单,在工作中,定位问题的思路基本一致。
在这里插入图片描述
内存溢出的原因有很多,比如代码中存在大对象分配,导致没有足够的内存空间存放该对象;再比如应用存在内存泄漏,导致在多次垃圾收集之后,依然无法找到一块足够大的内存容纳当前对象。

对于堆溢出的解决方法,这里提供如下思路:

  • (1)检查是否存在大对象的分配,最有可能的是大数组分配。
  • (2)通过jmap命令,把堆内存dump下来,使用内存分析工具分析导出的堆内存文件,检查是否存在内存泄漏的问题。
  • (3)如果没有找到明显的内存泄漏,考虑加大堆内存。
  • (4)检查是否有大量的自定义的Finalizable对象,也有可能是框架内部提供的,考虑其存在的必要性。

3、OOM案例2:元空间溢出

方法区与堆一样,是各个线程共享的内存区域,它用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JDK 8后,元空间替换了永久代来作为方法区的实现,元空间使用的是本地内存。

Java虚拟机规范对方法区的限制非常宽松,除了和堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当元空间无法满足内存分配需求时,将抛出OOM异常。元空间溢出报错信息如下:

     java.lang.OutOfMemoryError:Metaspace

元空间溢出可能有如下几种原因:

  • (1)运行期间生成了大量的代理类,导致元空间被占满,无法卸载。
  • (2)应用长时间运行,没有重启。
  • (3)元空间内存设置过小。

该类型内存溢出解决方法有如下几种:

  • (1)检查是否永久代空间或者元空间设置得过小。
  • (2)检查代码中是否存在大量的反射操作。
  • (3)dump之后通过mat检查是否存在大量由于反射生成的代理类。

代码清单如下所示,代码含义是使用动态代理产生类使得元空间溢出。
在这里插入图片描述
JVM参数配置如下:
在这里插入图片描述
浏览器发送如下请求:

     http://localhost:8080/metaSpaceOom

运行结果如下所示:
在这里插入图片描述
查看监控,如下图所示:
在这里插入图片描述
可以看到,Full GC非常频繁,而且元空间占用了59190KB即57.8MB空间,几乎把整个元空间占用。所以得出的结论是方法区空间设置过小,或者存在大量由于反射生成的代理类。查看GC日志如下:
在这里插入图片描述
在这里插入图片描述
可以看到Full GC是由于元空间不足引起的,那么接下来分析到底是什么数据占用了大量的方法区。导出dump文件,使用Java VisualVM分析。

首先确定是哪里的代码发生了问题,可以通过线程来确定,因为在实际生产环境中,有时候无法确定是哪块代码引起的OOM,那么就需要先定位问题线程,然后定位代码,如下图所示:
在这里插入图片描述
定位到问题线程之后,使用MAT工具打开继续分析,如下图所示,先打开线程视图,然后根据线程名称打开对应线程的栈信息,最后找到对应的代码块。
在这里插入图片描述
定位到代码以后,发现有使用到cglib动态代理,那么猜想问题是由于产生了很多代理类。接下来,可以通过包看一下类加载情况。由于代码是代理的People类,所以直接打开该类所在的包,如下图所示:
在这里插入图片描述
可以看到确实加载了很多的代理类,想一下解决方案,是不是可以只加载一个代理类以及控制循环的次数,当然如果业务上确实需要加载很多类的话,就要考虑增大方法区大小和控制循环的次数,所以这里修改代码如下:

     enhancer.setUseCache(true);

修改代码enhancer.setUseCache(false)。当设置为true的话,表示开启cglib静态缓存,这样每次动态代理的结果是生成同一个类。再看程序运行结果如下:

     …
     我是print本人
     class com.yang.jvmdemo.bean.People$$EnhancerByCGLIB$$65398cd
     totalClass:6872
     activeClass:6872
     unloadedClass:0
     我是加强类哦,输出print之前的加强方法
     …

可以看到,生成代理类的数量几乎不变,元空间也没有溢出。到此,问题解决。如果需要生成不同的类,调整代码更改循环次数即可。

4、OOM案例3:GC overhead limit exceeded

出现GC overhead limit exceeded这个错误是由于JVM花费太长时间执行GC,且只能回收很少的堆内存。根据Oracle官方文档表述,默认情况下,如果Java进程花费98%以上的时间执行GC,并且每次只有不到2%的堆被恢复,则JVM抛出GC overhead limit exceeded错误。换句话说,这意味着应用程序几乎耗尽了所有可用内存,垃圾收集器花了太长时间试图清理它,并多次失败。这本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出,GC overhead limit exceeded异常的最终结果是Java heap space。

在这种情况下,用户会体验到应用程序响应非常缓慢,通常只需要几毫秒就能完成的某些操作,此时则需要更长的时间来完成,这是因为所有的CPU正在进行垃圾收集,因此无法执行其他任务。使用代码清如下演示GC overhead limit exceeded异常。
在这里插入图片描述
JVM配置如下所示:
在这里插入图片描述
test1()方法的含义是运行期间将内容放入常量池,运行结果是GC overhead limit exceeded错误。test2()方法的含义是不停地追加字符串str,运行结果是Java heap space错误。大家可能会疑惑,看似test1()方法和test2()方法也没有太大的差别,为什么test2()方法没有报GC overhead limit exceeded呢?以上两个方法的区别在于发生Java heap space的test2()方法每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出。发生GC overhead limit exceeded的test1()方法由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。

需要注意的是,有些版本的JDK,有可能不会发生GC overhead limit exceeded,各位知道即可。该案例报错信息如下:
在这里插入图片描述
通过查看GC日志可以发现,系统在频繁地做Full GC,但是却没有回收多少空间,那么引起的原因可能是内存不足,也可能是存在内存泄漏的情况,接下来我们要根据堆内存文件具体分析GC overhead limit exceeded的原因。

1、定位问题代码块:
通过线程分析,可以定位发生OOM的代码块,如下图所示:
在这里插入图片描述
2、分析堆内存文件:
可以看到发生OOM是因为死循环,不停地往ArrayList存放字符串常量,JDK 1.7以后,字符串常量池移到了堆中存储,所以最终导致内存不足发生了OOM。

打开“Histogram”选项,如下图所示。可以看到,String类型的字符串占用了大概7.5M的空间,几乎把堆占满,但是还没有占满,所以这也符合官方对此异常的定义。
在这里插入图片描述
右击选择“List objects”,列出上中对象下面的所有引用对象,如下图所示,可以看到所有String对象。
在这里插入图片描述
3、解决方案
这个是JDK 6新加的错误类型,一般都是堆空间不足导致的。针对该问题的解决方法如下:

  • (1)检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
  • (2)添加JVM参数-XX:-UseGCOverheadLimit禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现java.lang.OutOfMemoryError:Java heap space。
  • (3)导出堆内存文件,如果没有发生内存泄漏,加大内存即可。

5、OOM案例4:线程溢出

线程溢出报错信息如下:

     java.lang.OutOfMemoryError :unable to create new native Thread

线程溢出是因为创建的了大量的线程。出现此种情形之后,可能造成系统崩溃。代码清单如下模拟了线程溢出。
在这里插入图片描述
结果如下:
在这里插入图片描述
JDK 5.0以后栈默认为1MB,以前栈默认为256KB。根据应用的线程所需内存大小进行调整,通过参数-Xss设置栈内存。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值是3000~5000。

操作系统能创建的线程数的具体计算公式如下:

   (MaxProcessMemory - JVMMemory - ReservedOsMemory)/(ThreadStackSize)=
Number of threads

其中各项代表含义如下:

  • (1)MaxProcessMemory表示进程可寻址的最大空间。
  • (2)JVMMemory表示JVM内存。
  • (3)ReservedOsMemory表示保留的操作系统内存。
  • (4)ThreadStackSize表示线程栈的大小。

在Java语言里,JVM在创建一个Thread对象的同时创建一个操作系统线程,而这个系统线程的内存用的不是JVMMemory,而是系统中剩下的内存(RemainMemory),计算公式如下:

     MaxProcessMemory - JVMMemory – ReservedOsMemory = RemainMemory

由公式得出:JVM分配内存越多,那么能创建的线程越少,越容易发生java.lang.OutOfMemoryError:unable to create new native thread。

针对该问题的解决方案如下:

  • (1)如果程序中有bug,导致创建大量不需要的线程或者线程没有及时回收,那么必须解决这个bug,修改参数是不能解决问题的。
  • (2)如果程序确实需要大量的线程,现有的设置不能达到要求,那么可以通过修改MaxProcessMemory、JVMMemory和ThreadStackSize三个因素,来增加能创建的线程数。比如使用64位操作系统可以增大MaxProcessMemory、减少JVMMemory的分配或者减小单个线程的栈大小。

在实验过程中,64位操作系统下调整Xss的大小并没有对产生线程的总数产生影响,程序执行到极限的时候,操作系统会死机,无法看出效果。

在32位Win7操作系统下测试,发现调整Xss的大小会对线程数量有影响,随着Xss值的变大,线程数量越来越少。如下表所示,其中JDK版本是1.8(适配32位操作系统)。
在这里插入图片描述
Xss参数的调整对于64位操作系统的实验结果是不明显的,但是对于32位操作系统的实验结果却是非常明显的,为什么会有这样的区别呢?上面讲到过线程数量的计算公式如下所示:

   (MaxProcessMemory - JVMMemory - ReservedOsMemory)/(ThreadStackSize)=
Number of threads

MaxProcessMemory表示最大寻址空间,在32位系统中,CPU的寻址范围就受到32个二进制位的限制。32位二进制数最大值是11111111 11111111 11111111 11111111,2的32次方=4294967296B = 4194304KB = 4096M =4GB。也就是说32位CPU只能访问4GB的内存。再减去显卡上的显存等内存,可用内存要小于4GB,所以32位操作系统可用线程数量是有限的。

64位二进制数的最大值是11111111 11111111 1111111111111111 11111111 11111111 11111111 11111111,2的64次方=17179869184GB,大家可以看看64位操作的寻址空间大小比32位操作系统多了太多,所以这也是我们总是无法测试出很好效果的原因。

综上,在生产环境下如果需要更多的线程数量,建议使用64位操作系统,如果必须使用32位操作系统,可以通过调整Xss的大小来控制线程数量。除此之外,线程总数也受到系统空闲内存和操作系统的限制。

6、小结

讲解了常见的内存溢出场景,针对不同的场景分析了出现异常的原因,并给出了不同的解决方案。本帖重点讲解了遇到问题时,对问题的解决思路。在工作中,业务场景会更加复杂,内存溢出问题也更加难以解决,这就需要花更多的精力和时间去认真分析问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1591813.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

今天我们来聊一聊Java中的Semaphore

写在开头 在上几天写《基于AQS手写一个同步器》时,很多同学留言说里面提到的Semaphore,讲得太笼统了,今天趁着周末有空,咱们就一起详细的学习和梳理一把 Semaphore。 什么是Semaphore? 在前面我们讲过的synchronize…

【计算机毕业设计】人事管理系统——后附源码

🎉**欢迎来到我的技术世界!**🎉 📘 博主小档案: 一名来自世界500强的资深程序媛,毕业于国内知名985高校。 🔧 技术专长: 在深度学习任务中展现出卓越的能力,包括但不限于…

【刷题】代码随想录算法训练营第十一天|20、有效的括号,1047、删除字符中的所有相邻重复项,150、逆波兰表达式求值

目录 20、有效的括号1047、删除字符中的所有相邻重复项150、逆波兰表达式求值 20、有效的括号 讲解:https://programmercarl.com/0020.%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7.html 括号匹配是使用栈解决的经典问题。 三种不匹配:第一种是缺少匹…

Axure RP中的相关概念及高保真原型构建方法

1 Axure RP中概念介绍 对于构建高保真原型来说,需要知道事件(Event)、Case、Action等概念。Axure RP中给出这些概念,是为了方便原型的构建,尤其是高保真原型的构建。 事件(Event)是附着于控件…

.vue文件引入路径正确,但报错

问题描述 使用Vue挂载组件时,导入路径正确,但是一直提示 Already included file name ‘绝对路径/index.vue’ differs from file name ‘绝对路径/Index. vue’ only in casing. The file is in the program because: Imported via ‘./components/ind…

Ubuntu,Kylin环境使用clock()函数设置延迟

一、Ubuntu操作系统中&#xff0c;直接在main中测试clock()设置延迟功能 代码描述&#xff1a;直接在main中使用clock()函数设置200ms延迟。 代码输出&#xff1a; 实现了200ms的延迟。 #include <time.h> #include <sys/time.h> #include <stdio.h> #inc…

HarmonyOS实战开发-自定义通知角标、如何设定应用的桌面图标角标的功能。

介绍 本示例主要展示了设定应用的桌面图标角标的功能&#xff0c;使用ohos.notificationManager 接口&#xff0c;进行桌面角标的设置&#xff0c;通知的发送&#xff0c;获取等。 效果预览 使用说明 在使用本应用时&#xff0c;需安装并启动仿桌面应用&#xff1b;在主界面…

微信小程序报错——“errno“: 600001, “errMsg“: “request:fail -2:net::ERR_FAILED“

bug现象 微信小程序体验版和真机调试 进入小程序的时候接口就出现了这个报错 "errno": 600001, "errMsg": "request:fail -2:net::ERR_FAILED" 排查 检查是证书过期还是证书链不完整 证书的信任链完整问题&#xff0c;可以在 亚数信息-SSL/TLS安…

Leetcode:283.移动零

题目要求 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums [0,1,0,3,12] 输出: [1,3,12,0,0] 示例 2: 输入: nums [0…

8、【构造者模式】适合于需要创建复杂对象的场景

你好&#xff0c;我是程序员。 今天我们来学习23种设计模式中的建造者模式。构造者模式是什么&#xff1f;有什么优缺点&#xff1f;使用场景&#xff0c;与工厂模式有什么区别&#xff1f;简单代码实现。 一、是什么&#xff1f; 建造者模式&#xff08;Builder Pattern&…

MySQL:MySQL的查询(上)

文章目录 MySQL的增加单行数据插入多行数据插入插入否则更新替换 MySQL的查询select列where语句 本篇开始总结的是MySQL当中的基本查询语句 对于数据库的查询&#xff0c;无非大致就是增删查改&#xff0c;因此对于这些内容进行一一解释&#xff1a; MySQL的增加 单行数据插…

鸿铭网创88计第49计:2024挂机托管项目, 单号躺赚4000+,无需发作品

项目概述&#xff1a; 这个副业项目极为适合忙碌却希望额外赚钱的伙伴们。核心是与平台合作&#xff0c;提供快手账号用于发布视频&#xff0c;视频中会嵌入推广链接。当有用户通过这些链接下载应用时&#xff0c;我们就能获得拉新佣金。 项目 地 址 &#xff1a; laoa1.cn/…

Json和Qt中Json的使用学习笔记

视频链接 https://www.bilibili.com/video/BV1yf4y1A7ek/?p2&spm_id_frompageDriver&vd_sourcefa4ef8f26ae084f9b5f70a5f87e9e41b Json JSON是在网络传输中常用的数据格式&#xff0c;能将不同类型的数据统一起来&#xff0c;我们在发送数据前将不同类型的数据存入到…

关于Git的一些基础用法

关于Git的一些基础用法 1. 前言2. 使用GitHub/gitee创建项目2.1 创建账号2.2 创建项目2.3 下载仓库到本地2.4 提交代码到远端仓库2.5 查看日志2.6 同步远端仓库和本地仓库 1. 前言 首先说一个冷知识&#xff08;好像也不是很冷&#xff09;&#xff0c;Linux和git的创始人是同…

c语言多功能计算软件170

定制魏&#xff1a;QTWZPW&#xff0c;获取更多源码等 目录 题目 要求 主要代码片段 题目 设计一个计算器软件&#xff0c;具备如下功能提示界面。 要求 设计出界面&#xff0c;注意界面名称最后为自己的姓名&#xff1b;&#xff08;20分&#xff09;能够实现加、减、乘、…

BoostCompass(建立正排索引和倒排索引模块)

阅读导航 一、模块概述二、编写正排索引和倒排索引模块✅安装 jsoncpp✅Jieba分词库的安装1. 代码基本框架2. 正排索引的建立3. 倒排索引的建立 三、整体代码⭕index.hpp 一、模块概述 这个模块我们定义了一个名为Index的C类&#xff0c;用于构建和维护一个文档索引系统。该系…

AI python

AI python 软件方面程序上的人工智能&#xff0c;和物理那种能跑机器人没关系

emmet语法---快速生成css样式

前言 本文是对emmet语法生成css样式的整理&#xff0c;emmet语法就相当于快捷键一样&#xff0c;只需要我们输入简单的缩写&#xff0c;然后按下tab键即可生成麻烦的css代码。 emmet语法生成css代码 其实&#xff0c;特别简单。比html还要简单。 先上案例&#xff1a; 我们输…

ControllerAdvice用法

ControllerAdvice用法 ControllerAdvice是一个组件注解&#xff0c;它允许你在一个地方处理整个应用程序控制器的异常、绑定数据和预处理请求。这意味着你不需要在每个控制器中重复相同的异常处理代码&#xff0c;从而使得代码更加简洁、易于管理。 主要特性 全局异常处理&a…

Python单元测试框架—pytest常用测试报告类型

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 先前博客有介绍pytest测试框架的安装及使用&#xff0c;现在来聊…