Java Agent的隔離實現(xiàn)以及卸載時一些坑

在《一文帶你了解Java Agent》中,讓大家了解了Java Agent的來龍去脈,當(dāng)通過attach方式去動態(tài)加載一個Java Agent時,Agent中的類會被加載到業(yè)務(wù)的虛擬機(jī)中,在使用完Agent的之后,如果想卸載這些無用的類,怎么實現(xiàn)?

這里就涉及到如何回收Perm區(qū)、或者M(jìn)etaspace中已經(jīng)加載的類了,如果一個類的類加載器對象沒有GC Root關(guān)聯(lián),那么可以通過FGC的方式回收這些類。不過,如果通過JVM內(nèi)部的類加載器比如AppClassLoader去加載這些類的話,可能永遠(yuǎn)也不能回收了,所以得通過自定義的類加載器去實現(xiàn)Agent類的加載動作,因為自定義的類加載器對象,我們可以自己控制。

下面是自定義類加載器的實現(xiàn)

public class AgentClassLoader extends URLClassLoader {

    public AgentClassLoader(URL[] urls) {
        super(urls, ClassLoader.getSystemClassLoader().getParent());
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        final Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            if (resolve) {
                resolveClass(loadedClass);
            }
            return loadedClass;
        }

        // 優(yōu)先從parent(SystemClassLoader)里加載系統(tǒng)類,避免拋出ClassNotFoundException
        if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {
            return super.loadClass(name, resolve);
        }

        // 先從agent中加載
        try {
            Class<?> aClass = findClass(name);
            if (resolve) {
                resolveClass(aClass);
            }
            return aClass;
        } catch (Exception e) {
            // ignore
        }
        return super.loadClass(name, resolve);
    }
}

這樣,通過AgentClassLoader加載的類,就可以和業(yè)務(wù)的類完全隔離開,在需要回收這些類的時候,只要把AgentClassLoader對象和GC root的關(guān)聯(lián)完全掐斷就行。

不過用了AgentClassLoader之后,還是遇到了一些坑,比如在Agent中使用Cat的時候,因為Cat是單例模式,都是通過Cat.logEvent這種方式使用,所以在第一次使用Cat的時候,Cat內(nèi)部會進(jìn)行初始化,比如系統(tǒng)信息上報邏輯。因為業(yè)務(wù)邏輯在使用Cat的時候,已經(jīng)初始化過了一次,在Agent內(nèi)部使用時,因為是通過AgentClassLoader加載的,又是一個全新的Cat,相當(dāng)于那些上報邏輯又初始化了一次,這這種明顯是不行的,那如何在Agent中可以使用業(yè)務(wù)加載的那個Cat對象呢?

后來想到了一個解決方案,通過一個CatAdapt封裝了一下Cat

public class CatAdapter {

    private static final Logger logger = LoggerFactory.getLogger(CatAdapter.class);
    private static Method logEvent;

    public static void init(ClassLoader classLoader) {
        try {
            Class catClazz = Class.forName("com.dianping.cat.Cat", true, classLoader);
            logEvent = catClazz.getMethod("logEvent", String.class, String.class);
        } catch (Exception e) {
            logger.error("cat adapter init failed", e);
        }
    }

    public static void logEvent(String type, String name) {
        if (logEvent != null) {
            try {
                logEvent.invoke(null, type, name);
            } catch (Exception e) {
               // ignore
            }
        }
    }
}

在Agent初始化入口的agentmain方法中,獲取當(dāng)前線程的classLoader

ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
Class catAdapter = agentLoader.loadClass("com.**.**.CatAdapter");
Method catAdapterInit = catAdapter.getMethod("init", ClassLoader.class);
catAdapterInit.invoke(null, currentClassLoader);

又通過agentLoader去加載CatAdapter類,在init方法中,通過當(dāng)前線程的classLoader去加載真正的Cat類,這時拿到的Cat的class對象和業(yè)務(wù)的Cat class對象是同一個,從而避免了上述問題,在Agent內(nèi)部就可以通過CatAdapter實現(xiàn)Cat方法的代理調(diào)用,實現(xiàn)數(shù)據(jù)的埋點。

卸載時的一些坑

為了驗證執(zhí)行FGC時,是否可以把無用的類回收,遇到了下面這些坑。
1、很單純的以為把a(bǔ)gentLoader設(shè)置為null,我就可以快樂的回收了,執(zhí)行了jmap -histo:live pid之后,驚喜的發(fā)現(xiàn),Agent的類還在。
2、為了看下為什么沒有回收,把堆對象dump下來,通過mat工具進(jìn)行分析,找了一個Agent的類,發(fā)現(xiàn)其對象正被agentLoader對象拽著,順騰摸瓜,發(fā)現(xiàn)agentLoader被線程池的線程拽著,這下明白了,需要把這些線程池給shutdown掉
3、因為在Agent初始化的時候,創(chuàng)建了幾個線程池處理一些內(nèi)部邏輯,所以要卸載Agent的時候,這些線程池必須shutdown。
4、把線程池shutdown之后,繼續(xù)使用jmap -histo:live pid,發(fā)現(xiàn)這些類特么還在,真是頑固啊。dump下來,繼續(xù)分析,發(fā)現(xiàn)agentLoader還被一個Finalizer對象給勾著!這是為啥,為什么有Finalizer對象勾著它?按照我的理解,只有重寫了finalize方法的類才會有Finalizer對象,一瞬間,我懷疑是不是線程池的類重寫了finalize方法,一查還真是,在ThreadPoolExecutor類中重寫了finalize方法。

5、重寫了finalize方法,這種情況理論上要經(jīng)過兩次GC才會被回收,執(zhí)行了兩次jmap -histo:live pid,Agent的類果然沒了!!!那個開心。
6、后面又一次不經(jīng)意的發(fā)現(xiàn)又無法回收了,又只能dump下來,繼續(xù)分析,這次agentLoader對象被業(yè)務(wù)線程的threadLocal對象給拽著了,死都不放手。

這一次真的查了好久,因為不好復(fù)現(xiàn),前前后后驗證了多次,發(fā)現(xiàn)在使用了Agent的Mock功能之后,就會出現(xiàn)這個問題,Mock功能會根據(jù)業(yè)務(wù)配置的String字符串,通過jackson框架反序列化成一個對象并返回。

jackson在序列化的時候,需要開辟一塊內(nèi)存空間,為了能夠重復(fù)利用這塊空間,jackson默認(rèn)把這個內(nèi)存空間封裝成一個SoftReference保存在ThreadLocal中。

這樣每個線程都有一塊內(nèi)存可以重復(fù)使用,這原本是好事,但是在我們這,變成了一只暗搓搓的手,死死抓著agentLoader不放,導(dǎo)致了所有類都不能回收。

JsonFactory f = new JsonFactory();
f.disable(JsonFactory.Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING);

最終通過取消這個特性,每次序列化都去創(chuàng)建一塊內(nèi)存,這樣就可以避免這個問題,又可以快樂的回收了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容