Metaspace内存不足导致FGC问题排查
事件回顾
清楚的记得是2020/7/25 14:34分左右,周六的下午,我还在公司苦逼的加班中,突然钉钉告警群里出现大量应用OP的dubbo超时调用、空指针异常,异常中间还有Metaspace元空间不足等异常:
o.a.c.f.l.ListenerContainer 98 [ERROR] Listener (org.apache.curator.framework.recipes.cache.PathChildrenCache$3@7edb7fd5) threw an exception
java.lang.OutOfMemoryError: Metaspace
错误类型:【oom】
告警内容:2020-07-25 15:05:05:113 d5f54db7c1ca49ab85b9f54cde234bd1 c.c.d.l.w.DriverTraceWriterUtil 39 [ERROR] driver trace writer to file fail,ex:[{}]
java.lang.RuntimeException: by java.lang.ClassFormatError: Metaspace
at com.caocao.dc
再紧接着,发现我们应用OP的服务器大量FullGC,先一台发生,很快第二台开始FGC,第10台...
2020-07-25T15:10:50
应用:xxx
主机:xxx(
agentId: yyyy
发生FGC,共耗时:25012ms
2020-07-25T15:10:25
应用:xxxx
主机:xxxx
agentId: yyy
发生FGC,共耗时:4223ms
涉及到对OP系统调用的各系统都在反馈出现dubbo调用超时,都在报错中,我们通过pinpoint也发现应用频繁发生了FGC:
1.png
2.png
上面我们大概可以判断出来,是由于Metaspace元空间不足,出现内存溢出,导致jvm频繁触发full GC,为了保证业务正常,此时我们让运维紧急重启了服务器,通过重启服务器,业务逐渐恢复正常,元空间使用量也降下来了。在发生FGC时让运维dump内存了,后面会分析该文件。
JVM参数
OP应用的生成JVM参数如下:
/usr/local/java/bin/java
-server #指定JVM的启动模式是client模式还是server模式
-Xms4g #初始化堆内存4G,堆内存最小值
-Xmx4g #最大堆4g
-Xmn2g #年轻代2G,老年代大小=Xmx-Xmn
-Xss512k #每个线程的堆栈大小
-XX:MetaspaceSize=256m #元空间的初始大小
-XX:MaxMetaspaceSize=512m #元空间最大值
-XX:-UseGCOverheadLimit #预测是否要OOM了,提前抛出异常,防止OOM发生
-XX:+DisableExplicitGC #禁用System.gc()
-XX:+UseConcMarkSweepGC #指定老年代的收集算法使用CMS,会默认使用ParNew作为新生代收集器
-XX:+CMSParallelRemarkEnabled #开启并行标记,减少停顿时间
-XX:+UseCMSCompactAtFullCollection #FULL GC时对老年代进行压缩。CMS默认不会移动内存,因此容易产生碎片。增加该参数虽然会影响性能,但可以消除碎片
-XX:+UseFastAccessorMethods #正确获取方法的调用计数,以便VM可以更好地识别代码中的热点
-XX:+UseCMSInitiatingOccupancyOnly #指定HotSpot VM总是使用-XX:CMSInitiatingOccupancyFraction的值作为老年代使用率限制来启动CMS垃圾回收。如果没有使用-XX:+UseCMSInitiatingOccupancyOnly,那么HotSpot VM只是利用CMSInitiatingOccupancyFraction启s动第一次CMS垃圾回收,后面都是使用HotSpot VM自动计算出来的值
-XX:CMSInitiatingOccupancyFraction=70 #CMS垃圾收集器,老年代使用率达到70%时,触发CMS垃圾回收
-XX:LargePageSizeInBytes=128m #堆内存大页的大小,大的内存分页可以增强 CPU 的内存寻址能力,从而提升系统的性能
-Djava.awt.headless=true
-Djava.net.preferIPv4Stack=true
-Ddubbo.application.qos.port=12881
-javaagent:/usr/local/pinpoint/pinpoint-bootstrap-1.6.0.jar
-Dpinpoint.agentId=driver-op-...
-Dpinpoint.applicationName=OP
-Djava.ext.dirs=/usr/local/springboot/OP/lib:/usr/local/java/jre/lib/ext
-XX:+HeapDumpOnOutOfMemoryError #当堆内存空间溢出时输出堆的内存快照,配合-XX:HeapDumpPath使用
-XX:HeapDumpPath=/home/admin #当堆内存空间溢出时输出堆的内存快照输出目录
-cp /usr/local/springboot/OP/conf:.:/usr/local/java/lib:/usr/local/java/jre/lib -jar /usr/local/springboot/OP/OP.jar
由配置的JVM参数知道,指定了CMS为老年代的垃圾收集器,默认ParNew为新生代垃圾收集器,最大堆4g,老年代2g,年轻代2g,年轻中Eden区域和Survivor区域(From幸存区或To幸存区)的默认比例为8, 即设置survivor:eden=2:8(From:TO:eden=200MB:200MB:1600MB),元空间初始化大小256MB,最大值512MB,如果老年代空间使用率达到70%,会触发CMS垃圾回收。由pinpoint上可以看出,元空间使用大概在770MB左右,超过了最大元空间值,导致元空间内存不足,触发FGC,这里有个疑问,明明配置的最大512MB,为什么使用了770MB,Metaspace还有一个区间是Klass Metaspace,由参数-XX:CompressedClassSpaceSize进行控制,JDK8的时候 Klass Metaspace默认是1G。
原因分析
- MAT分析
使用MAT打开dump文件,点开
Histogram
柱状图,选择java.lang.Class
,右击选择List objects
,选择with incoming references(当前查看的对象,被外部引用)
,查看通过这个class创建的类信息:
发现创建了大量Proxy
类,右击选中Path To GC Roots
,选中exclude all phantom/weak/soft etc.references
(排除虚引用/弱引用/软引用等的引用链,被虚引用/弱引用/软引用的对象可以直接被GC给回收,要看该对象否还存在Strong引用链,如果有,则说明存在内存泄漏):
发现Proxy
类被org.springframework.boot.loader.LaunchedURLClassLoader
强引用,导致生成的Proxy
类无法被卸载一直残留在MetaSpace区造成内存泄漏。
- 代码分析
上面分析出来生成
Proxy
类可能存在内存泄漏,代码中会发现用动态代理创建Proxy
类对象并放入WeakReference中,每次GC时该对象都会被回收,会重复创建Proxy
类对象,而且类加载器不会被回收,导致类不会被卸载。具体代码参考了dubbo代码com.alibaba.dubbo.common.bytecode.Proxy#getProxy(java.lang.ClassLoader, java.lang.Class<?>...)
。 - 解决方法
上层业务做缓存处理,不会重复创建
Proxy
对象。上线观察优化前后5天内的元空间增长,的确效果比较明显。
参考文章
- https://www.jianshu.com/p/738b4f3bc44b
- https://www.cnblogs.com/throwable/p/12216546.html
- https://blog.csdn.net/a15939557197/article/details/90635460?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param
- Codeforces Round #409 (rated, Div. 2, based on VK Cup 2017 Round 2)(A.思维题,B.思维题)
- 设计模式六大原则(1):单一职责原则
- 设计模式六大原则(2):里氏替换原则
- Selenium2+python自动化72-logging日志使用
- Codeforces Round #395 (Div. 2)(A.思维,B,水)
- php实现图形计算器
- Selenium2+python自动化73-定位的坑:class属性有空格
- 华中农业大学第五届程序设计大赛网络同步赛题解
- Java构造方法与析构方法实例剖析
- 5.训练模型之利用训练的模型识别物体
- KMP算法学习(详解)
- 查找算法的实现(C/C++实现)
- HDU 1495 非常可乐(数论,BFS)
- Selenium2+python自动化74-jquery定位
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 该了解一波了!零基础入门Nginx
- 轻松一刻——LeetCode题目13:罗马数字转整数
- 动动手——LeetCode题目14:最长公共前缀
- LeetCode题目15:三数之和
- 三数之和姊妹题——LeetCode题目16:最接近的三数之和
- 组合问题——LeetCode题目17:电话号码的字母组合
- Python读取PDF文档并翻译
- n数之和题目要类比——LeetCode题目18:四数之和
- SpringBoot使用MySQL访问数据
- MySQL数据库与JDBC编程
- 自动删除QQ空间指定好友的留言
- 在Ubuntu 18.04中安装VMware工具
- 微信小程序下拉刷新功能
- 详解Linux Screen让程序保持后台运行
- Python Des加密与解密实现软件注册码、机器码