JVM的Perm区持续增长导致OOM问题记录

来源:互联网 发布:intent传递数据 编辑:程序博客网 时间:2024/06/11 12:36

查找问题

先用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@0x00000007e3f853a80x0000000793e8ad28  20  279464  0x000000077b328568  dead    groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a80x000000078e3106f8  20  279464  0x000000077b328568  dead    groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a80x000000077bf13df0  1   3072    0x0000000778325b10  dead    sun/reflect/DelegatingClassLoader@0x00000007e005c4c80x000000079ed982d8  20  279464  0x000000077b328568  dead    groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a80x000000079d4954c0  20  279464  0x000000077b328568  dead    groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a80x000000077a3df5c8  1   3080    0x0000000778325b10  dead    sun/reflect/DelegatingClassLoader@0x00000007e005c4c80x00000007ae218838  20  279464  0x000000077b328568  dead    groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a80x000000077a441f58  1   3072    0x0000000778325b10  dead    sun/reflect/DelegatingClassLoader@0x00000007e005c4c80x000000078c6ea450  20  279464  0x000000077b328568  dead    groovy/lang/GroovyClassLoader$InnerLoader@0x00000007e3f853a80x000000077a3f9718  1   1896    0x0000000778325b10  dead    sun/reflect/DelegatingClassLoader@0x00000007e005c4c80x000000077a3f5a58  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包。

1 0
原创粉丝点击