记一次使用visualVM分析GroovyClassLoader加载机制导致频繁gc的性能问题

一、现象描述

    通常使用如下代码在Java 中执行 Groovy 脚本:

1 GroovyClassLoader groovyLoader = new GroovyClassLoader();
2 Class<Script> groovyClass = (Class<Script>) groovyLoader.parseClass(groovyScriptFile);
3 Script groovyScript = groovyClass.newInstance();

    时,每次执行groovyLoader.parseClass(groovyScriptFile),Groovy 为了保证每次执行的都是新的脚本内容,会每次生成一个新名字的Class文件对象。但当对同一段脚本每次都执行这个方法时,会导致的现象就是装载的Class会越来越多,从而导致PermGen占用过高,甚至被用满。如下图1所示。

 图 1

二、现象分析

    visualVM是一款优秀的性能分析工具,可从idea的plugins中下载,具体操作和解释可以看这篇文章 。

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

 图 2

三、现象解决

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

   1. 原始的调用方式

复制代码
 1 Object scriptObject = null;
 2 try {
 3     Binding binding = new Binding();
 4     binding.setVariable("context", this.context);
 5     binding.setVariable("clientInfo", clientInfo);
 6     binding.setVariable("params", params);
 7     binding.setVariable("data", data);
 8 
 9     GroovyShell shell = new GroovyShell(binding);
10     scriptObject = (Object) shell.evaluate(script);
11 } catch (Throwable t) {
12     log.error("groovy script eval error. script: " + script, t);
13 }
14 
15 return scriptObject;
复制代码

  2. 为Groovy Script增加缓存

复制代码
 1 private Map<String, Object> scriptCache = new ConcurrentHashMap<String, Object>();
 2 ...
 3 
 4 Object scriptObject = null;
 5 try {
 6     Binding binding = new Binding();
 7     binding.setVariable("context", this.context);
 8     binding.setVariable("clientInfo", clientInfo);
 9     binding.setVariable("params", params);
10     binding.setVariable("data", data);
11 
12     Script shell = null;
13     if (isCached(cacheKey)) {
14         shell = (Script) getCaches().get(cacheKey);
15     } else {
16         shell = new GroovyShell().parse(script);
17     }
18 
19     shell.setBinding(binding);
20     scriptObject = (Object) shell.run();
21 
22     // clear
23     binding.getVariables().clear();
24     binding = null;
25 
26     // Cache
27     if (!isCached(cacheKey)) {
28         shell.setBinding(null);
29         getCaches().put(cacheKey, shell);
30     }
31 } catch (Throwable t) {
32     log.error("groovy script eval error. script: " + script, t);
33 }
34 
35 return scriptObject;
复制代码

  3. 解决Binding的线程安全问题

复制代码
 1 private Map<String, Object> scriptCache = new ConcurrentHashMap<String, Object>();
 2 ...
 3 
 4 Object scriptObject = null;
 5 try {
 6     Binding binding = new Binding();
 7     binding.setVariable("context", this.context);
 8     binding.setVariable("clientInfo", clientInfo);
 9     binding.setVariable("params", params);
10     binding.setVariable("data", data);
11 
12     Script shell = null;
13     if (isCached(cacheKey)) {
14         shell = (Script) getCaches().get(cacheKey);
15     } else {
16         shell = cache(cacheKey, script);
17     }
18 
19     scriptObject = (Object) InvokerHelper.createScript(shell.getClass(), binding).run();
20 
21     // Cache
22     if (!isCached(cacheKey)) {
23         getCaches().put(cacheKey, shell);
24     }
25 } catch (Throwable t) {
26     log.error("groovy script eval error. script: " + script, t);
27 }
28 
29 return scriptObject;
复制代码

 

posted @   屠城校尉杜  阅读(1097)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示