【Aviator】(三)缓存引起的Full GC解决

【背景】

【Aviator】(三)缓存引起的Full GC解决_第1张图片

    1.压测试算接口, 每分钟3600次请求

    2.每1次试算需计算30个公式

    3.通过监控,发现在上述条件下,30mins内老年代每分钟触发一次垃圾回收

  (备注:老年代触发的 gc 是 full gc 会导致应用进程停顿 对性能的影响比较大)

 

【思路】

       1.静态变量的生命周期会伴随jvm全程,我封装的工具类中,使用了很多静态方法,因对象对其有引用,是否导致堆内存的对象一直没有释放,一点点的内存泄漏,导致堆内存可用空间越来越小,最终内存溢出,触发full gc?

       2.我的代码里是否有大对象?导致其存入堆内存后直接进入老年代?

       看这内存配置也还行,给老年代预留的空间还是可以的:

-Xms2048m -Xmx2048m -Xmn512m -Xss256k -XX:PermSize=256m -XX:MaxPermSize=256m -XX:SurvivorRatio=8

 

【实践】

   1.review 潜在问题的piece

【Aviator】(三)缓存引起的Full GC解决_第2张图片

      结合我在第2篇aviator中的设计图,有如下几个点可能出问题的点:

      1.每次试算从redis拉取一次公式集合"formulaMap",期间此Map要经过反射转换为ConcurrentHashMap且2个map我预设空间均为256。(ConcurrentHashMap对key和value要求同时都不能为null)

      2.每次计算时,需要envMap做入参(obj转concurrentHashMap),且后续计算需要不断往进put新的计算中间值,初识大小128。     

/**
 * map转concurrentHashMap
 */
public static ConcurrentHashMap copyMaptoTsMap(Map paramsMap, ConcurrentHashMap resultMap) throws BusinessException{
    isNull(paramsMap,"线程安全转换,formulaMap入参为空");
    if (resultMap == null) {
        resultMap = new ConcurrentHashMap(256);
    }
    Iterator it = paramsMap.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry entry = (Map.Entry) it.next();
        Object key = entry.getKey();
        resultMap.put(key, paramsMap.get(key) != null ? paramsMap.get(key) : "");
    }
    return resultMap;
}

/**
 * obj转concurrentHashmap
 */
public static ConcurrentHashMap beanToTsMap(Object obj) throws BusinessException{
    if(obj == null){
        return null;
    }
    ConcurrentHashMap map = new ConcurrentHashMap(128);
    try {
        Field[] declaredFields = obj.getClass().getDeclaredFields();
        for (Field field : declaredFields) {
            field.setAccessible(true);
            if(null == field || null == field.getName() || null == field.get(obj)){
                continue;
            }
            map.put(field.getName(), field.get(obj));
        }
        if(map.containsKey("serialVersionUID")){
            map.remove("serialVersionUID");
        }
    } catch (Exception e) {
        throw new BusinessException("表达式引擎,bean转concurrentHashmap异常!");
    }
    return map;
}

      3.每次试算无论何种资金来源,都要连续计算共30个公式,做30次表达式compile编译(官方推荐、性能更高)、execute执行操作。

      4.期间封装多为静态方法,但是没有用到静态变量。

 

    2.控制变量监测内存

       针对上述4个点,分别避开做实验,比如从redis并发拉去公式,改为实例化map;每次3个资金来源的公式,我改为1种资金,并缩小envMap容量;静态工具改为注入,非静态方法等。然而依旧每分钟1次full gc发生。

       之后思考是否aviator连续计算30个公式导致的,毕竟1分钟3600次调用,就要执行10800次表达式引擎的编译和执行,修改每次只计算贷款金额,发现并未出现老年代的垃圾回收。大致问题定位在aviator框架比较吃内存。

       但之后,对照pom,aviator用的是最推荐的新版本,同时先compile后execute的方式也是官方最推荐的,那就奇了怪了,难道aviator的性能并没有官方推荐那么好吗?

 

3.jmap分析

      此处推荐一篇文章:https://blog.csdn.net/kevin_luan/article/details/8447896 通过jmapdump分析dump文件,最好结合MemoryAnalyzer工具来查看可视化界面。

      (在家用的笔记本,就不截图了)

      通过jmap在测试服务器上进行分析,每经历1000次试算,发现2个现象:

      1.永久带内存会增加40mb的占用,且不会释放。

      2.堆内存占用整体容量并未明显增长,且这个过程中新生代垃圾回收很频繁。

      此处更加说明了,我的业务代码不是导致频繁老年代full gc的原因。

 

4.aviator官方查询issue

      带着上述的问题,在此查看aviator官方内容,发现前人提过类似的issue:

       经过在官方github一通儿乱点,发现点儿门道:

       “每个表达式都会生成一个匿名类(java lambda 对应的匿名类),因此如果你的表达式都是动态生成的,那么可能会生成大量的匿名类,占满 metaspace 就会触发 full gc。”, 原来坑在这里终于找到了。

      备注:metaspece和老年代

在jdk1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

 

【方案】

        aviator通常采取对方式是直接执行表达式, 事实上 Aviator 背后都帮你做了编译并执行的工作。当然你可以自己先编译表达式, 返回一个编译的结果, 然后传入不同的env来复用编译结果, 提高性能, 这是更推荐的使用方式。示例:

    public class TestAviator {
        public static void main(String[] args) {
            String expression = "a-(b-c)>100";
            // 编译表达式
            Expression compiledExp = AviatorEvaluator.compile(expression);
            Map env = new HashMap();
            env.put("a", 100.3);
            env.put("b", 45);
            env.put("c", -199.100);
            // 执行表达式
            Boolean result = (Boolean) compiledExp.execute(env);
            System.out.println(result);  // false
        }
    }

         由于每分钟有3600*30个编译结果需要程序自己存这些匿名类,所以官方说了如果频繁调用建议缓存编译结果:

public static Expression compile(String expression, boolean cached)

        将cached设置为true即可, 那么下次编译同一个表达式的时候将直接返回上一次编译的结果。 我在第二篇中介绍了之前的设计方案,其中将编译后的表达式缓存到内存中,通过id来检索,问题在于分布式部署,修改公式时,nginx只能打到一台机器,官方的做法则是直接缓存表达式本身,不通过id来查找,当时怎么就没想到呢。。。。

 

        That's all.

        2019.04.05 (清明节)

 

          

你可能感兴趣的:(【Aviator】(三)缓存引起的Full GC解决)