最近解决了几次内存异常的问题,有两次是堆外内存异常,感觉解决的问题越多,问题的共性就越容易总结,在这里给大家分享一下,希望抛砖引玉能够帮大家解决遇到的问题。
其实有了MAT这类工具,一般堆内内存基本都能借助工具分析出大概问题所在,但堆外内存有时就不能直观地发现问题了,从解决过几次线上问题的现象总结,堆外内存过高80%都是这两种因素引起:
- 若metaspace正常,有可能是线程数过多造成的
- 若metaspace异常,有可能是classLoader过多造成的
当然了,并不是说只有这两种情况,有些也可能是直接内存泄露的问题,但如果你的项目不是大量操作直接内存,或者使用netty等第三方框架的话,可以考虑以上两个问题。
本次文章主要分享metaspace异常的解决过程。前段时间发现某个服务每隔几天就重启一次,由于使用k8s集群,进程内存达到某个阀值会被kill掉重启,通过jvm监控发现被kill之前的堆内内存十分正常,而metaspace却缓慢地上升:
然后通过jmap命令将内存dump出来后,利用mat工具打开,发现其classLoader数量特别多:
PS:由于这里只列出重复类,所以数量没有对上
通过查看该classLoader的gc roots可以看出其引用路径:
说实话,一开始看到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出文件:
只有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,这就导致堆外内存过大的问题
来源:oschina
链接:https://my.oschina.net/u/1985414/blog/3167182