一、前言
最近给客户调优过程中,频繁遇到 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();}
}
}