这次调优横跨java和Groovy(SimpleTemplateEngine)原创
一、前言
最近给客户调优过程中,频繁遇到java调用groovy的情况,在排查过程中也发现了一些相关的性能瓶颈。其中比较突出的是调用groovy api时导致的频繁类加载问题,就这个问题在本地模拟了客户相关的代码实现,进行分析解决。
二、问题现象
(一)高CPU消耗
1. 加载groovy jar时的路经检查
2. 读取jar包时的解压缩操作
(二)类加载耗时
最直观是压测过程中,相关的单接口响应时间很长,至少是秒级起步,这边就不放相关的截图了。
三、问题排查
以下内容以调用SimpleTemplateEngine获取占位符的值为例,以下为本地模拟情况,重在排查思路。
(一)第一次优化
1. 线程dump,在dump文件中,线程栈中个方法调用一目了然
在某个线程中,我们找到了类加载相关的方法,直接看最近的自定义类的方法,一开始千万不要陷入各种框架或组件的方法。这里看到是TestSimpleTemplateEngine
类的generateMessage方法。
直接反编译类,查看方法如下:
每次调用此方法都会new SimpleTemplateEngine类(调优过程中,客户那边的代码就是这么实现的),而且每个客户端的请求都会调用到这个方法。
2. 尝试单例解决
看到这里,第一个优化思路就是将new SimpleTemplateEngine()这个操作使用单例实现。
当然结果是:毫无成效。没办法,只能从源码开始了。
(二)源码开始,第二次优化
1. SimpleTemplateEngine类
阅读SimpleTemplateEngine
类源码,发现它在实例化的时候根本没做啥骚操作,就是设置了个成员属性groovyShell
。真正涉及到类加载器的是SimpleTemplateEngine
的createTemplate
方法。
2. createTemplate方法
createTemplate源码如下:
重点都在这个方法里,通过传入的文本模版解析groovy脚本等一系列操作,其中会把文本脚本封装成一个GroovyCodeSource
类,注意这边它会自动给你的脚本生成一个groovy后缀名结尾的文件名。
filename中counter是自增,所以每次调用createTemplate
生成的filename都是不一样的。后面将给定的groovy code转换成java类的时候,会进行类加载。类加载的之前会检查缓存(HashMap)中是否已存在class类,如果存在则不会调用类加载器进行类加载。
直接跳到开始解析类的地方:
具体加载过程如上源码。其中缓存的检查是就是根据上面提到的GroovyCodeSource
的name
属性。那么问题来了,这个name每次生成的时候每次都会变化,也就是说这边类加载的时候永远用不到缓存,每次都需要调用类加载器进行类加载。
3. 开始真正的优化了
缓存大法好。既然真正导致类加载的是createTemplate
方法,就直接把createTemplate生成的Template实例给缓存了。缓存用什么呢?最简单直接的就是Map了。当然如果涉及的模版类型比较多,用Map的话可能会占用大量内存,不怕不是还有Guava,Caffeine这种高性能本地缓存框架吗,LRU耍起来,会过期总比每次类加载来的好吧。当然如果每个模版类型都是不一样的,加不加缓存效果都一样,这种情况暂时还没想到解决方法。
四、优化后结果
所有的线程栈中都没有类加载的影子了(因为打印了返回值,日志量比较大,都在刷日志);而且如果关注优化前后的GC情况的话,会发现优化后GC情况好了不是一点点,在同样-Xmx1g
的情况下,调优前会频繁的Full GC,优化后只有Minor GC。
五、测试代码
- 优化前
public class TestSimpleTemplateEngine {
private final static Logger log = LoggerFactory.getLogger(TestSimpleTemplateEngine.class);
private final static ArrayList strTemplates = new ArrayList();
static {
strTemplates.add("${user.name}");
strTemplates.add("${user.code}");
strTemplates.add("${user.company}");
strTemplates.add("${user.address}");
strTemplates.add("${user.message}");
}
public static String generateMessage(Map map, String placeHolder) {
String msg = null;
try {
msg = new SimpleTemplateEngine().createTemplate(placeHolder).make(map).toString();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return msg;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (;;) {
Map<String,Object> map = new HashMap<>();
int nameSuffix = new Random().nextInt(900) + 100;
int index = new Random().nextInt(strTemplates.size());
// 这里随便整个POJO都行,只要相关的属性对的上就行
Person userDo = new Person();
userDo.setName("TestGroovy" + nameSuffix);
userDo.setCode(666);
// 添加域对象
map.put("user",userDo);
String placeHolder = (String) strTemplates.get(index);
String userName = generateMessage(map, placeHolder);
log.info(placeHolder + ": " + userName + Thread.currentThread().getName());
}
}).start();
}
}
}
- 优化后
public class TestSimpleTemplateEngineAfterTuning {
private final static Logger log = LoggerFactory.getLogger(TestSimpleTemplateEngineAfterTuning.class);
// 添加模版类缓存
private final static ConcurrentHashMap<String, Template> templateCaches = new ConcurrentHashMap<>();
private final static ArrayList strTemplates = new ArrayList();
static {
strTemplates.add("${user.name}");
strTemplates.add("${user.code}");
strTemplates.add("${user.company}");
strTemplates.add("${user.address}");
strTemplates.add("${user.message}");
}
public static Template getTemplate(String placeHolder) throws IOException, ClassNotFoundException {
Template template = templateCaches.get(placeHolder);
if (template != null) return template;
template = new SimpleTemplateEngine().createTemplate(placeHolder);
templateCaches.put(placeHolder, template);
return template;
}
public static String generateMessage(Map map, String placeHolder) {
String msg = null;
try {
msg = getTemplate(placeHolder).make(map).toString();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return msg;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (; ; ) {
Map<String, Object> map = new HashMap<>();
int nameSuffix = new Random().nextInt(900) + 100;
int index = new Random().nextInt(strTemplates.size());
Person userDo = new Person();
userDo.setName("TestGroovy" + nameSuffix);
userDo.setCode(666);
map.put("user", userDo);
String placeHolder = (String) strTemplates.get(index);
String userName = generateMessage(map, placeHolder);
log.info(placeHolder + ": " + userName + Thread.currentThread().getName());
}
}).start();
}
}
}