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包。
- JVM的Perm区持续增长导致OOM问题记录
- JVM中Perm区持续上涨问题
- JVM中Perm区持续上涨问题
- JVM中Perm区持续上涨问题
- JVM中Perm区持续上涨问题
- JVM中Perm区持续上涨问题
- 测试Perm区溢出引起的OOM以及原因分析
- 【iOS自动约束】使用Masonry导致内存持续增长问题分析
- 深入JVM的OOM
- jvm的OOM
- JVM成长之路,记录一次内存溢出导致频繁FGC的问题排查及解决
- Android中解决图像解码导致的OOM问题
- Android中解决图像解码导致的OOM问题
- Android中解决图像解码导致的OOM问题
- 【Android问题及其解决】又见图片导致的OOM
- 几种jvm OOM问题
- Hadoop命令执行时提示JVM OOM问题的处理
- max_map_count超出导致的OOM
- Java高级编程-JUC
- |BZOJ 2028|平衡树|[SHOI2009]会场预约
- 编译工具
- TIJ......(一)
- Zookeeper安装
- JVM的Perm区持续增长导致OOM问题记录
- gcc和g++
- 详解JavaScript操作URL的方法(单页应用常用)
- python之路第一站
- 1056. 组合数的和(15)
- request中参数(parameter)和属性(Attribute)的区别
- Java提高篇——JAVA三大特征之继承
- ListView视图动态增加与删除控件条目,内容
- mysql 菜鸟优化