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