其实有了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 {@linkjava.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 {@linkjava.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了(低版本跟泄露原因有关,且看下面)。
至此,问题的根源已经找到,我的做法是将groovy升级到2.5,目前服务正常运行。
而另一次由于线程数过高而导致堆外内存异常的情况,是有人配了server.tomcat.min-spare-threads=500,并且没有指定栈空间大小,按java官网的描述linux默认1M,这就导致堆外内存过大的问题