Groovy引发的PermGen区爆满问题定位与解决

更新时间:2016-01-07 17:32:02 点击次数:2137次

现象与定位

jstack

由于集群有流量的增长,以及开放新的服务出去,所以首先看了一下线程使用情况,未发现问题。

$JAVA_HOME/bin/jstack -l PID > jstack.out
grep -A 2 -B 5 -i "com.xxxxxx.xxxx" ./jstack.out 

jstat

使用jstat查看GC情况发现PermGen将满,并且频繁触发FGC,虽然能够回收无效的类,但产生类的速度比FGC的效率还快,直接导致CPU使用率飙升。

ps -ef | grep java
$JAVA_HOME/bin/jstat -gcutil PID 1000 100 

图1 PermGen区升到95%引发频繁FGC

jmap & MAT

遇到线上FGC问题常用的工具是用jmap & eclipse MAT来分析JVM内存使用情况了。网上资料很多,这里不在赘述,需要注意的一点是jmap是会触发"stop the world"的,所以好是拉出来,然后做dump操作。如果用eclipse MAT分析切记提前把eclipse大能使用的内存调整下,否则分析上G的dump文件会挂掉。

图2 Dump中的类加载器情况

由于是PermGen区的泄漏,通过分析发现类加载部分有大量的GroovyClassLoader,这时想到有使用两个服务,内部实现是使用Groovy配置的方式(这里我用Groovy封装了一个服务提供的微框架)。但这两个服务其实使用的并不频繁,所以导致切换后的现象不是雪崩式的集群挂掉,而是慢慢的,逐步有机器load飙高告警。

解决方法

定位到问题,事情就好办了,首先看一下处理Groovy加载执行的代码;然后为生成的类加一层对象缓存;由于脚本中用到了Binding上下文对象,为了线程安全性,调整执行时的方式。后问题得到解决。

原始的调用方式

Object scriptObject = null;
try {
    Binding binding = new Binding();
    binding.setVariable("context", this.context);
    binding.setVariable("clientInfo", clientInfo);
    binding.setVariable("params", params);
    binding.setVariable("data", data);

    GroovyShell shell = new GroovyShell(binding);
    scriptObject = (Object) shell.evaluate(script);
} catch (Throwable t) {
    log.error("groovy script eval error. script: " + script, t);
}

return scriptObject; 

为Groovy Script增加缓存

private Map<String, Object> scriptCache = new ConcurrentHashMap<String, Object>();
...

Object scriptObject = null;
try {
    Binding binding = new Binding();
    binding.setVariable("context", this.context);
    binding.setVariable("clientInfo", clientInfo);
    binding.setVariable("params", params);
    binding.setVariable("data", data);

    Script shell = null;
    if (isCached(cacheKey)) {
        shell = (Script) getCaches().get(cacheKey);
    } else {
        shell = new GroovyShell().parse(script);
    }

    shell.setBinding(binding);
    scriptObject = (Object) shell.run();

    // clear
    binding.getVariables().clear();
    binding = null;

    // Cache
    if (!isCached(cacheKey)) {
        shell.setBinding(null);
        getCaches().put(cacheKey, shell);
    }
} catch (Throwable t) {
    log.error("groovy script eval error. script: " + script, t);
}

return scriptObject; 

解决Binding的线程安全问题

private Map<String, Object> scriptCache = new ConcurrentHashMap<String, Object>();
...

Object scriptObject = null;
try {
    Binding binding = new Binding();
    binding.setVariable("context", this.context);
    binding.setVariable("clientInfo", clientInfo);
    binding.setVariable("params", params);
    binding.setVariable("data", data);

    Script shell = null;
    if (isCached(cacheKey)) {
        shell = (Script) getCaches().get(cacheKey);
    } else {
        shell = cache(cacheKey, script);
    }

    scriptObject = (Object) InvokerHelper.createScript(shell.getClass(), binding).run();

    // Cache
    if (!isCached(cacheKey)) {
        getCaches().put(cacheKey, shell);
    }
} catch (Throwable t) {
    log.error("groovy script eval error. script: " + script, t);
}

return scriptObject; 

小结

这次碰到的问题还是很具有典型性的,其中的思路和用到的工具可以阅读这两本JVM相关的书籍获取:《深入理解Java虚拟机》和《Java性能优化权威指南》。

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!