一次Full GC [Metadata GC Threshold] 问题分析

GC日志

取了2条最新的GC日志做对比
第一条是系统刚发布完不就机器重启后,做了一次heap dump,触发了一次Full GC
第二条是系统运行2天后,发生了一次Full GC


image.png

可以看出是由于Metadata GC Threshold引起的,而且耗时居然有10s,非常恐怖

2022-05-14T13:10:37.806+0800: 178995.554: 
[Full GC (Metadata GC Threshold) 2022-05-14T13:10:37.807+0800: 178995.554: 
[CMS: 2198976K->2012812K(5242880K), 9.7080398 secs] 2627723K->2012812K(6186624K), 
[Metaspace: 575152K->575152K(1906688K)], 10.1689786 secs] 
[Times: user=8.84 sys=1.32, real=10.17 secs] 

Heap dump下载

MAT分析

image.png

看看MAT默认的内存泄露分析报告

Problem Suspect1

KieContainerFactory类里面,ConcurrentHashMap$Node[]这个类的实例出现了非常多次,内存占比达到了15.22%

查看代码,KieContainerFactory内部确实有一个ConcurrentHashMap属性,map里面的对象是在Spring init的时候初始化进去的,我们这个域是执行规则引擎drools的服务,KieContainer的创建逻辑很重,所以需要预热放到内存中,规则执行的时候直接创建KieSession,然后执行。查看了一下,系统正常运行的规则有几千个,所以看起来这个内存占用是正常的业务需要。

Problem Suspect2

ClassLoaderWeavingAdaptor类出现了9200+个实例,而且这些实例都是被 java.util.HashMap$Node[]这个类的实例引用指向,总共内存占用达到了60%,非常可怕。

这个看起来是aspectj框架的类,里面存的到底是啥呢,我们慢慢分析

首先,打开Histogram视图,按照Retained Heap倒排,第一个就是我们要找的


image.png

选中这行,右键 list objects -> with outgoing references
发现这个类的实例有10000+

image.png

继续选中第一行,右键 list objects -> with incoming references

发现是 org.aspectj.weaver.loadtime.Aj#WeaverContainer 这个类里面,有一个名称为 weavingAdaptors,类型为 java.utils.Collections.synchronizedMap的属性,而这个map的key是org.aspectj.weaver.loadtime.AjAdaptorKey,value是org.aspectj.weaver.loadtime.AjExplicitlyInitializedClassLoaderWeavingAdaptor,而ExplicitlyInitializedClassLoaderWeavingAdaptor这个内部类里面又有一个weavingAdaptor的对象

image.png

通过查看源代码,以下就是我们要找的对象


image.png
image.png

那么为什么会有那么多ClassLoaderWeavingAdaptor,原因是map里面存放了很多个node,每个node都指向一个。我们具体来看一下这个map里面存放的对象,找到那个map,找到outgoing references,仔细看会发现key都是一个AdaptorKey对象,而这个AdaptorKey在构造方法会传入一个ClassLoader对象,这个ClassLoader目前看起来就是org.drools.dynamic.DynamicProjectClassLoader。而且往下一直检查,发现绝大多数AdaptorKey都是这个DynamicProjectClassLoader

image.png

这个类一下子引起了我的注意,是我们用到的drools框架中的自定义ClassLoader,难道又是drools规则太多,导致了很多自定义类加载器?

为了搞清楚这个问题,我想得先搞清楚aspectj这个框架,弄明白这个Aj.java到底是干什么的。 通过反复谷歌百度,还有公司内部confluence搜索,得到了一些启发。

由于公司内部有一套自研的调用链跟踪系统,使用到了aspectj无侵入式埋点,而我们项目的启动参数确实配置了 -javaagent


image.png

aspectj在做类的织入之前,会调用org.aspectj.weaver.loadtime.Aj#preProcess方法,方法内部会进行WeaverContainer(即weavingAdaptors)的初始化,在创建weavingAdaptor之前会有一个是否跳过的判断,这个loadersToSkip 实际上读取了系统属性配置。

public static List<String> loadersToSkip = null;

    static {
        // pr271840 - touch the types early and outside the locks
        new ExplicitlyInitializedClassLoaderWeavingAdaptor(new ClassLoaderWeavingAdaptor());
        try {
            String loadersToSkipProperty = System.getProperty("aj.weaving.loadersToSkip","");
            StringTokenizer st = new StringTokenizer(loadersToSkipProperty, ",");
            if (loadersToSkipProperty != null && loadersToSkip == null) {
                if (st.hasMoreTokens()) {
//                  System.out.println("aj.weaving.loadersToSkip is set. Skipping loaders: '"+loadersToSkipProperty+"'");
                    loadersToSkip = new ArrayList<String>();
                }
                while (st.hasMoreTokens()) {
                    String nextLoader = st.nextToken();
                    loadersToSkip.add(nextLoader);
                }
            }
        } catch (Exception e) {
            // Likely security issue related to property access...
        }
    }
public byte[] preProcess(String className, byte[] bytes, ClassLoader loader, ProtectionDomain protectionDomain) {
        if (loader == null || className == null || 
            loader.getClass().getName().equals(deleLoader) || loader.getClass().getName().equals(deleLoader2)) {
            // skip boot loader, null classes (hibernate), or those from a reflection loader
            return bytes;
        }
        // ----------------------- !!! 这里可以自定义跳过的类加载器
        if (loadersToSkip != null) {
            // Check whether to reject it
            if (loadersToSkip.contains(loader.getClass().getName())) {
//              System.out.println("debug: no weaver created for loader '"+loader.getClass().getName()+"'");
                return bytes;
            }
        }

        if (trace.isTraceEnabled())
            trace.enter("preProcess", this, new Object[] { className, bytes, loader });
        if (trace.isTraceEnabled())
            trace.event("preProcess", this, new Object[] { loader.getParent(), Thread.currentThread().getContextClassLoader() });

        try {
            synchronized (loader) {

                if (SimpleCacheFactory.isEnabled()) {
                    byte[] cacheBytes= laCache.getAndInitialize(className, bytes,loader,protectionDomain);
                    if (cacheBytes!=null){
                            return cacheBytes;
                    }
                }

//------------------------- !!!关键代码在这里,get的时候如果没有则创建
                WeavingAdaptor weavingAdaptor = WeaverContainer.getWeaver(loader, weavingContext);
                if (weavingAdaptor == null) {
                    if (trace.isTraceEnabled())
                        trace.exit("preProcess");
                    return bytes;
                }
                try {
                    weavingAdaptor.setActiveProtectionDomain(protectionDomain);
                    byte[] newBytes = weavingAdaptor.weaveClass(className, bytes, false);
                    Dump.dumpOnExit(weavingAdaptor.getMessageHolder(), true);
                    if (trace.isTraceEnabled())
                        trace.exit("preProcess", newBytes);
                    if (SimpleCacheFactory.isEnabled()) {
                        laCache.put(className, bytes, newBytes);
                    }
                    return newBytes;
                } finally {
                    weavingAdaptor.setActiveProtectionDomain(null);
                }
            }

            /* Don't like to do this but JVMTI swallows all exceptions */
        } catch (Throwable th) {
            trace.error(className, th);
            Dump.dumpWithException(th);
            // FIXME AV wondering if we should have the option to fail (throw runtime exception) here
            // would make sense at least in test f.e. see TestHelper.handleMessage()
            if (trace.isTraceEnabled())
                trace.exit("preProcess", th);
            return bytes;
        } finally {
            CompilationAndWeavingContext.resetForThread();
        }
    }

那么,现在weavingAdaptors里有很多Map.Node的原因找到了,是因为有很多DynamicProjectClassLoader,每个加载器在加载类的时候 ,都会被Aj.preClass拦截,都会生产一个ClassLoaderWeavingAdaptor对象。(至于为什么会有这么多类加载器,本机调试发现是由于每个KieContainer创建的时候都有单独的类加载器,而且不止这一个,具体原因不赘述)
因为生产必须使用aspectj实现全链路跟踪,那么我们就要针对这个自定义类加载器做优化,所以我们就需要指定跳过

-Daj.weaving.loadersToSkip=org.drools.core.rule.JavaDialectRuntimeData$PackageClassLoader,org.drools.dynamic.DynamicProjectClassLoader

看到这里,或许你跟我一样,以为解决了,终于可以松一口气了。还好我沉得住气,先修改配置,再观察一下,才能跟领导汇报结果。
没想到,改完重启后,Heap Usage确实降下来了,但是Metaspace Usage仍然线性上升。

优化之前

image.png

优化之后

image.png

这个时候,才发现自己多么愚蠢,最开头的GC日志已经明确显示是Metaspace GC Threshold,而我找了半天问题,解决的根本不是Metaspace的问题。埋头继续......

找到一些参考资料:
https://mp.weixin.qq.com/s/qgpMMR8-493-Y9uwWiMdRg
https://mp.weixin.qq.com/s/N7YKNGHL3g6oEXKUcTDlZg
http://lovestblog.cn/blog/2016/10/29/metaspace/

然后,通过配置jvm启动参数,观察类的加载和卸载
-XX:+TraceClassLoading -XX:+TraceClassUnloading

持续调用接口,发现有特别多com.googlecode.aviator.Expression的加载


image.png

项目中确实用到了Google aviator表达式,发现在对表达式进行编译时,AviatorEvaluator.compile 传参第二个值是false,没有对表达式的编译结果进行缓存,可能导致多次编译,然后引起类的反复加载,每一次都是动态创建一个类,这样就造成了Metaspace内存的泄露。

image.png

重新修改代码AviatorEvaluator.compile(expr, true)然后发布,继续观察内存,Metaspace used一直保持200多M,不再线性上升,commited也保持在低位(虽然相对其他项目来说,Metaspace这个使用率也比较高了,但是泄露是没有了,使用率较高可能还是跟规则引擎drools框架的使用有关)


image.png

至此,问题解决。虽然大部分时间都没找到核心线索,但借此机会也对MAT工具更熟悉了,对aspectj的框架有了进一步了解。
解决问题,方向很重要,Metaspace泄露,直接观察类的加载和卸载过程!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容