性能文章>这次调优横跨java和Groovy(SimpleTemplateEngine)>

这次调优横跨java和Groovy(SimpleTemplateEngine)原创

6月前
192626

一、前言

最近给客户调优过程中,频繁遇到java调用groovy的情况,在排查过程中也发现了一些相关的性能瓶颈。其中比较突出的是调用groovy api时导致的频繁类加载问题,就这个问题在本地模拟了客户相关的代码实现,进行分析解决。
419871564616afb924126e_fix732.png

二、问题现象

(一)高CPU消耗
1. 加载groovy jar时的路经检查
4073877522616af379e1779.png

2. 读取jar包时的解压缩操作

2090751851616af383b07c3.png

(二)类加载耗时

最直观是压测过程中,相关的单接口响应时间很长,至少是秒级起步,这边就不放相关的截图了。

三、问题排查

以下内容以调用SimpleTemplateEngine获取占位符的值为例,以下为本地模拟情况,重在排查思路。

(一)第一次优化

1. 线程dump,在dump文件中,线程栈中个方法调用一目了然

在某个线程中,我们找到了类加载相关的方法,直接看最近的自定义类的方法,一开始千万不要陷入各种框架或组件的方法。这里看到是TestSimpleTemplateEngine类的generateMessage方法。

2123266426616af482285a5 1.png

直接反编译类,查看方法如下:

2334876637616af52231528.png

每次调用此方法都会new SimpleTemplateEngine类(调优过程中,客户那边的代码就是这么实现的),而且每个客户端的请求都会调用到这个方法。

2. 尝试单例解决

看到这里,第一个优化思路就是将new SimpleTemplateEngine()这个操作使用单例实现。
当然结果是:毫无成效。没办法,只能从源码开始了。

(二)源码开始,第二次优化

1. SimpleTemplateEngine类

阅读SimpleTemplateEngine类源码,发现它在实例化的时候根本没做啥骚操作,就是设置了个成员属性groovyShell。真正涉及到类加载器的是SimpleTemplateEnginecreateTemplate方法。

2. createTemplate方法

createTemplate源码如下:

746229979616af6319c554 1.png

重点都在这个方法里,通过传入的文本模版解析groovy脚本等一系列操作,其中会把文本脚本封装成一个GroovyCodeSource类,注意这边它会自动给你的脚本生成一个groovy后缀名结尾的文件名。
filename中counter是自增,所以每次调用createTemplate生成的filename都是不一样的。后面将给定的groovy code转换成java类的时候,会进行类加载。类加载的之前会检查缓存(HashMap)中是否已存在class类,如果存在则不会调用类加载器进行类加载。
直接跳到开始解析类的地方:

2383827068616af69b1d330 1.png

具体加载过程如上源码。其中缓存的检查是就是根据上面提到的GroovyCodeSourcename属性。那么问题来了,这个name每次生成的时候每次都会变化,也就是说这边类加载的时候永远用不到缓存,每次都需要调用类加载器进行类加载。

3. 开始真正的优化了

缓存大法好。既然真正导致类加载的是createTemplate方法,就直接把createTemplate生成的Template实例给缓存了。缓存用什么呢?最简单直接的就是Map了。当然如果涉及的模版类型比较多,用Map的话可能会占用大量内存,不怕不是还有Guava,Caffeine这种高性能本地缓存框架吗,LRU耍起来,会过期总比每次类加载来的好吧。当然如果每个模版类型都是不一样的,加不加缓存效果都一样,这种情况暂时还没想到解决方法。
3043505068616afa11b15a9.png

四、优化后结果

所有的线程栈中都没有类加载的影子了(因为打印了返回值,日志量比较大,都在刷日志);而且如果关注优化前后的GC情况的话,会发现优化后GC情况好了不是一点点,在同样-Xmx1g的情况下,调优前会频繁的Full GC,优化后只有Minor GC。

1893744139616af7af18ca7.png

五、测试代码

  • 优化前
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();
        }
    }
}
请先登录,再评论

大佬多分享这种实际案例🙏

6月前

很好

6月前

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr