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

文章目录

  • 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$$65398cdtotalClass:6872activeClass:6872unloadedClass: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.mzph.cn/news/813289.shtml

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

相关文章

matlab2024a软件下载

matlab2024a软件下载 MATLAB R2024a版本终于来了,通过上一个版本连续更新至Update7就预感这个版本将带来更多的新功能及增强。MATLAB更新包括编辑器拼写检查、面板导航、局部函数、Python接口互操作性、REST函数服务、安全信息存储以及ode对象求解器。Simulink更新…

camera驱动学习总结记录

https://www.yuque.com/u2132176/yfiyal/ch1zsrgzevcwf1rw 视频教程里面对应的gc2053c驱动源码注解: gc2053.c(60 KB) 对应的驱动文档: Rockchip_Driver_Guide_VI_CN_v1.1.1(2).pdf(2.3 MB) 视频里面对应的mipi协议文档汇总: MIPI标准文档大…

lv_micropython to download and building

想要在ESP32-C3使用Micropython开发GUI,所以需要编译lv_micropython,当前github上的版本是9.1.0。 一、开发环境 因为编译lv_micropython需要在linux系统下,但是我的电脑是windows系统,所以我在windows系统上安装了VMware虚拟机&…

Django框架设计原理

相信大多数的Web开发者对于MVC(Model、View、Controller)设计模式都不陌生,该设计模式已经成为Web框架中一种事实上的标准了,Django框架自然也是一个遵循MVC设计模式的框架。不过从严格意义上讲,Django框架采用了一种更…

C语言什么是指针? 什么是指针变量?

一、问题 指针是 C 语⾔中的⼀个重要概念,也是 C 语⾔中的⼀个重要特⾊。它的身影在整个 C 语⾔体系中都会出现,⽽且其概念也⼗分复杂,需要多加注意和思考。 二、解答 为了更好地弄清指针的概念,这⾥不得不先提到地址以及数据在内…

MySQL知识整理

MySQL知识整理 基础第一讲:基础架构:一条SQL查询语句是如何执行的?架构尽量减少长连接的原因和方案为什么尽量不要依赖查询缓存 索引第四讲:深入浅出索引(上)第五讲:深入浅出索引(下…

【前端面试3+1】14 路由跳转的方式、如何取消已经发送的ajax请求、如何按顺序发起三个ajax请求并按顺序返回、【两个数组的并集】

一、路由跳转的几种方式 1、页面跳转 使用超链接 <a> 标签&#xff1a;通过在页面中定义超链接&#xff0c;用户点击超链接后会跳转到指定的URL页面。使用重定向&#xff1a;服务器端可以通过设置HTTP响应头中的Location字段&#xff0c;将用户重定向到指定的URL页面。使…

Web3 的社会影响:数字社会的新时代

随着科技的不断进步和创新&#xff0c;人类社会正逐步进入数字化时代的新阶段。Web3 技术作为数字社会的重要组成部分&#xff0c;正在以前所未有的方式重塑着我们的社会生活和交往方式。本文将探讨 Web3 技术对社会的影响&#xff0c;以及它所带来的数字社会的新时代。 1. Web…

JVM基础第一篇

内存结构 程序计数器 1.定义 在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;每个线程都有一个独立的程序计数器&#xff0c;它是线程私有的&#xff0c;不会被线程切换所影响。 2.作用 记住下一条jvm指令的执行地址 3.特点 是线程私有的不会存在内存溢出 虚拟机…

数据结构——单链表(C语言版)

文章目录 一、链表的概念及结构二、单链表的实现SList.h链表的打印申请新的结点链表的尾插链表的头插链表的尾删链表的头删链表的查找在指定位置之前插入数据在指定位置之后插入数据删除pos结点删除pos之后的结点销毁链表 三、完整源代码SList.hSList.ctest.c 一、链表的概念及…

【算法分析与设计】全排列

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;算法分析与设计 ⛺️稳中求进&#xff0c;晒太阳 题目 给定一个不含重复数字的整数数组 nums &#xff0c;返回其 所有可能的全排列 。可以 按任意顺序 返回答案。 示例 示例 1&#xff1…

Transformer 结构浅析

Transformer 结构浅析 文章目录 Transformer 结构浅析Transformer 网络结构编码器位置编码多头注意力层Add&NormFeed Forward 解码器带掩码的多头注意力层多头注意力层 预测 Transformer 网络结构 Transformer模型的网络结构如图&#xff0c;且transformer结构主要分为两部…

二叉搜索树--搜索二维矩阵 II

题目描述 编写一个高效的算法来搜索 m * n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,…

HarmonyOS4 页面路由

Index.ets: import router from ohos.routerclass RouterInfo {// 页面路径url: string// 页面标题title: stringconstructor(url: string, title: string) {this.url urlthis.title title} }Entry // 入口組件 Component struct Index {State message: string 页面列表// …

react17+antd4 动态渲染导航菜单中的icon

在路由信息对照表中的icon可以有两种形式&#xff1a;一种是组件形式&#xff0c;一种是字符串形式的。 在antd4的Menu.Item和SubMenu中的icon属性的格式为&#xff1a; 1.组件形式 这种方法在渲染时很方便&#xff0c;与antd中的Menu.Item中的icon属性的形式是一致的&#…

计算机组成原理【CO】Ch2 数据的表示和应用

文章目录 大纲2.1 数制与编码2.2 运算方法和运算电路2.3 浮点数的表示和运算 【※】带标志加法器OFSFZFCF计算机怎么区分有符号数无符号数? 【※】存储排列和数据类型转换数据类型大小数据类型转换 进位计数制进制转换2的次幂 各种码的基本特性无符号整数的表示和运算带符号整…

微信小程序Skyline模式下瀑布长列表优化成虚拟列表,解决内存问题

微信小程序长列表&#xff0c;渲染的越多就会导致内存吃的越多。特别是长列表的图片组件和广告组件。 为了解决内存问题&#xff0c;所以看了很多人的资料&#xff0c;都不太符合通用的解决方式&#xff0c;很多需要固定子组件高度&#xff0c;但是瀑布流是无法固定的&#xf…

推荐一款轻量级的hosts文件编辑器(免安装版)

在管理和编辑hosts文件时&#xff0c;一款简单而有效的工具是非常重要的。下面推荐一款免安装版的轻量级hosts文件编辑器&#xff0c;让你轻松管理你的hosts文件。 windows系统默认hosts文件位置 下载地址&#xff1a;https://www.alipan.com/s/8kSns9eAi9f

CSS aspect-ratio属性设置元素宽高比

aspect-ratio 是CSS的一个属性&#xff0c;用于设置元素的期望宽高比。它设置确保元素保持特定的比例&#xff0c;不受其内容或容器大小的影响。 语法&#xff1a; aspect-ratio: <ratio>;其中 <ratio> 是一个由斜杠&#xff08;/&#xff09;分隔的两个数字&…

代码随想录第39天 | 62.不同路径 、 63. 不同路径 II

一、前言 参考文献&#xff1a;代码随想录 今天主要的题目是动态规划的路径问题&#xff0c;动态规划五要点&#xff1b; 1、确定dp数组&#xff0c;dp[i]代表什么i代表什么&#xff1b; 2、递推公式&#xff1b; 3、初始化dp数组&#xff1b; 4、遍历顺序&#xff1b; …