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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/csujiangyu/article/details/54882230

查找问题

先用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包。

展开阅读全文

没有更多推荐了,返回首页