先用jstack看看线程栈是否正常,确认正常后用jmap查看(因为线上用的OpenJDK,需要安装debuginfo包)堆中快照情况。jmap一些命令可能会造成JAVA进程挂起,特别是jmap -permstat会造成STW,程序无法响应。建议使用jmap命令应该与线上环境隔离才能用。
使用jmap -permstat发现大量dead状态的class对象,其中class为groovy/lang/GroovyClassLoader$InnerLoader。
class_loader classes bytes parent_loader alive? type <bootstrap> 2801 17853536 null live <internal> 0x0000000781d20040 20 279464 0x000000077b328568 dead groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a8 0x0000000793e8ad28 20 279464 0x000000077b328568 dead groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a8 0x000000078e3106f8 20 279464 0x000000077b328568 dead groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a8 0x000000077bf13df0 1 3072 0x0000000778325b10 dead sun/reflect/DelegatingClassLoader@0x00000007e005c4c8 0x000000079ed982d8 20 279464 0x000000077b328568 dead groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a8 0x000000079d4954c0 20 279464 0x000000077b328568 dead groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a8 0x000000077a3df5c8 1 3080 0x0000000778325b10 dead sun/reflect/DelegatingClassLoader@0x00000007e005c4c8 0x00000007ae218838 20 279464 0x000000077b328568 dead groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a8 0x000000077a441f58 1 3072 0x0000000778325b10 dead sun/reflect/DelegatingClassLoader@0x00000007e005c4c8 0x000000078c6ea450 20 279464 0x000000077b328568 dead groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a8 0x000000077a3f9718 1 1896 0x0000000778325b10 dead sun/reflect/DelegatingClassLoader@0x00000007e005c4c8 0x000000077a3f5a58 1 3072 0x0000000778325b10 dead sun/reflect/DelegatingClassLoader@0x00000007e005c4c8 ... total = 10414 180017 2395296248 N/A alive=1, dead=10413 N/A初步怀疑Groovy脚本的使用出现了问题。在ideaJ中用全文搜索程序groovy信息,发现有2个类中用到了groovy校验。其中有个类是最近新加的,怀疑是这个类校验时出现问题。
@NotNull(when = "groovy:_this.seatCode == null") @NotBlank private String customerId; @NotNull(when = "groovy:_this.customerId == null") @NotBlank private String seatCode;问题出在ValidatorAspect中的validator方法中。每次校验接口参数都会实例化一个net.sf.oval.Validator对象。这是没必要的。理由是:1.首先net.sf.oval.Validator是线程安全的,不用考虑线程安全问题;2.net.sf.oval.Validator对象比较重,每次实例化会浪费很多内存资源;3. net.sf.oval.Validator在执行groovy脚本校验时,threadScriptCache会缓存groovy脚本,如果每次重新生成该实例会导致缓存失效。
Class<? extends ValidatorAdapter> vda = p.adapter(); //如未指定适配器,则默认使用oval验证对象 if (vda.getName().equals(ValidatorAdapter.class.getName())) { if(o != null) { //当验证对象不为null,使用oval验证框架验证 net.sf.oval.Validator validator = new net.sf.oval.Validator(); List<ConstraintViolation> ret = validator.validate(o); ...groovy脚本生成class入口代码。由于每次校验的时候都会新生成net.sf.oval.Validator实例,造成缓存scriptCache每次都重新生成,这里的缓存失效,每次都会重新解析groovy脚本。而静态变量GROOVY_SHELL每次解析groovy脚本的时候,都会新生成class加载到Perm区,导致OOM的问题发生。
public class ExpressionLanguageGroovyImpl implements ExpressionLanguage { private static final Log LOG = Log.getLog(ExpressionLanguageGroovyImpl.class); private static final GroovyShell GROOVY_SHELL = new GroovyShell(); private final ThreadLocalObjectCache<String, Script> threadScriptCache = new ThreadLocalObjectCache<String, Script>(); public Object evaluate(final String expression, final Map<String, ? > values) throws ExpressionEvaluationException { try { final ObjectCache<String, Script> scriptCache = threadScriptCache.get(); Script script = scriptCache.get(expression); if (script == null) { script = GROOVY_SHELL.parse(expression); scriptCache.put(expression, script); } final Binding binding = new Binding(); for (final Entry<String, ? > entry : values.entrySet()) { binding.setVariable(entry.getKey(), entry.getValue()); } LOG.debug("Evaluating Groovy expression: {1}", expression); script.setBinding(binding); return script.run(); } catch (final Exception ex) { throw new ExpressionEvaluationException("Evaluating script with Groovy failed.", ex); } } ...为什么没有groory脚本生成的class没有被GC回收? 因为GROOVY_SHELL静态的,这个肯定是不能GC回收的。GROOVY_SHELL每次执行parse的时候会缓存class信息
private static final GroovyShell GROOVY_SHELL = new GroovyShell();GroovyClassLoader在parseClass时会缓存在sourceCache中,而缓存的key为Groovy脚本的名字,这个名字每次生成都不一样。所以class每次都会重新生成,这样做是为了动态执行Groovy的class。潜在的问题是class会被无限加载到虚拟机的Perm区中。
public class GroovyClassLoader extends URLClassLoader { public Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException { synchronized (sourceCache) { Class answer = (Class) sourceCache.get(codeSource.getName()); if (answer != null) return answer; // Was neither already loaded nor compiling, so compile and add to // cache. CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource()); SourceUnit su = null; if (codeSource.getFile() == null) { su = unit.addSource(codeSource.getName(), codeSource.getInputStream()); } else { su = unit.addSource(codeSource.getFile()); } ClassCollector collector = createCollector(unit, su); unit.setClassgenCallback(collector); int goalPhase = Phases.CLASS_GENERATION; if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT; unit.compile(goalPhase); answer = collector.generatedClass; for (Iterator iter = collector.getLoadedClasses().iterator(); iter.hasNext();) { Class clazz = (Class) iter.next(); setClassCacheEntry(clazz); } if (shouldCacheSource) sourceCache.put(codeSource.getName(), answer); return answer; } }上面的codeSource.getName()得到的是脚本的名字。脚本名字在GROOVY_SHELL生成,每次生成名字都不一样。
protected synchronized String generateScriptName() { return "Script" + (++counter) + ".groovy"; }静态实例化net.sf.oval.Validator
private static final net.sf.oval.Validator validator = new net.sf.oval.Validator();现在架构大都是SOA或者微服务架构,服务通过RPC调用大都是无状态的,一般情况下出现OOM情况是比较少的。大部分OOM原因不合理使用引入的第三方中间件或者第三方jar包。随着后续业务量增大,需要更多关注和研究引入的第三方中间件或者第三方jar包。
