JVM堆外内存异常增长的解决过程

最近解决了几次内存异常的问题,有两次是堆外内存异常,感觉解决的问题越多,问题的共性就越容易总结,在这里给大家分享一下,希望抛砖引玉能够帮大家解决遇到的问题。

其实有了MAT这类工具,一般堆内内存基本都能借助工具分析出大概问题所在,但堆外内存有时就不能直观地发现问题了,从解决过几次线上问题的现象总结,堆外内存过高80%都是这两种因素引起:

  • 若metaspace正常,有可能是线程数过多造成的
  • 若metaspace异常,有可能是classLoader过多造成的

当然了,并不是说只有这两种情况,有些也可能是直接内存泄露的问题,但如果你的项目不是大量操作直接内存,或者使用netty等第三方框架的话,可以考虑以上两个问题。

本次文章主要分享metaspace异常的解决过程。前段时间发现某个服务每隔几天就重启一次,由于使用k8s集群,进程内存达到某个阀值会被kill掉重启,通过jvm监控发现被kill之前的堆内内存十分正常,而metaspace却缓慢地上升:

up-2da33c7142fdb6dea71892d921a658f63f6.png

然后通过jmap命令将内存dump出来后,利用mat工具打开,发现其classLoader数量特别多:

up-c5e3787d280aaf73e89ee0a4fd5e14a4068.png

up-f85518638f3412ce18a196e17c500f3f91e.png

PS:由于这里只列出重复类,所以数量没有对上

通过查看该classLoader的gc roots可以看出其引用路径:

up-5f92f1a5a334a45dc17ea38e7fe8a0a8bf7.png

说实话,一开始看到groovyClassLoader有点懵,想不到哪里用到groovy,猜测是第三方依赖,于是通过maven查看依赖树,发现是jsonPath引进来的org.codebaus.groovy2.4.5,而服务使用其解析json。然后下载了groovy2.4.5的源码,根据上图的gc roots查看相关代码,然后发现org.codehaus.groovy.reflection.GroovyClassValuePreJava7的类注释:

/** Approximation of Java 7's {@link java.lang.ClassValue} that works on earlier versions of Java.
 * Note that this implementation isn't as good at Java 7's; it doesn't allow for some GC'ing that Java 7 would allow.
 * But, it's good enough for our use.
 */

重点在于“it doesn't allow for some GC'ing that Java 7 would allow”,也就是说它有可能无法gc!但我明明使用的是java8呀,为何还用GroovyClassValuePreJava7呢?只好查看其引用位置,发现是通过org.codehaus.groovy.reflection.GroovyClassValueFactory#createGroovyClassValue创建的,通过系统参数groovy.use.classvalue=true/false控制使用GroovyClassValuePreJava7或GroovyClassValueJava7,默认前者。于是我尝试设置groovy.use.classvalue=true,服务运行24小时后重新dump出文件:

up-ae899c0d7e44f82221594825851d45831c5.png

只有19个,证明classLoader成功释放!既然默认的有这个问题,为何它要这样设置呢?GroovyClassValueFactory的注释可以说明原因:

/**
 * This flag is introduced as a (hopefully) temporary workaround for a JVM bug, that is to say that using
 * ClassValue prevents the classes and classloaders from being unloaded.
 * See https://bugs.openjdk.java.net/browse/JDK-8136353
 */

看来是为了解决ClassValue的问题,于是GroovyClassValuePreJava7自己实现了类似ClassValue的功能,GroovyClassValuePreJava7注释:

/** Approximation of Java 7's {@link java.lang.ClassValue} that works on earlier versions of Java.
 * Note that this implementation isn't as good at Java 7's; it doesn't allow for some GC'ing that Java 7 would allow.
 * But, it's good enough for our use.
 */

咳咳,问题你自己也没解决这个问题呀(-_-|||),而且貌似只有2.4才有这个问题,2.5开始GroovyClassValueFactory就默认使用GroovyClassValueJava7了(低版本跟泄露原因有关,且看下面)。

说了那么多,究竟是什么原因造成classLoader泄露呢?这个问题比较复杂,大家可以到 https://issues.apache.org/jira/browse/GROOVY-7683 自行查看,看样子应该是跟org.codehaus.groovy.reflection.ClassInfo内部klazz的强引用有关,2.5以上已经改为弱引用了(具体改动请看 https://github.com/apache/groovy/commit/a8fb776023253ebc2da35538f25eccd7d59997ed )。而2.4的改动参考 https://github.com/apache/groovy/commit/97d78e9e52deb52c8e66db501ef208f30384d014 ,可以看出正是在这个版本加了klazz强引用。

至此,问题的根源已经找到,我的做法是将groovy升级到2.5,目前服务正常运行。

而另一次由于线程数过高而导致堆外内存异常的情况,是有人配了server.tomcat.min-spare-threads=500,并且没有指定栈空间大小,按java官网的描述linux默认1M,这就导致堆外内存过大的问题

up-1842382d11476310d6646963cca55217063.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值