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

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

這里就涉及到如何回收Perm區(qū)跑筝、或者M(jìn)etaspace中已經(jīng)加載的類了死讹,如果一個(gè)類的類加載器對(duì)象沒有GC Root關(guān)聯(lián),那么可以通過FGC的方式回收這些類继蜡。不過,如果通過JVM內(nèi)部的類加載器比如AppClassLoader去加載這些類的話逛腿,可能永遠(yuǎn)也不能回收了稀并,所以得通過自定義的類加載器去實(shí)現(xiàn)Agent類的加載動(dòng)作,因?yàn)樽远x的類加載器對(duì)象单默,我們可以自己控制碘举。

下面是自定義類加載器的實(shí)現(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ù)的類完全隔離開,在需要回收這些類的時(shí)候境蜕,只要把AgentClassLoader對(duì)象和GC root的關(guān)聯(lián)完全掐斷就行蝙场。

不過用了AgentClassLoader之后,還是遇到了一些坑粱年,比如在Agent中使用Cat的時(shí)候售滤,因?yàn)镃at是單例模式,都是通過Cat.logEvent這種方式使用,所以在第一次使用Cat的時(shí)候完箩,Cat內(nèi)部會(huì)進(jìn)行初始化赐俗,比如系統(tǒng)信息上報(bào)邏輯。因?yàn)闃I(yè)務(wù)邏輯在使用Cat的時(shí)候弊知,已經(jīng)初始化過了一次阻逮,在Agent內(nèi)部使用時(shí),因?yàn)槭峭ㄟ^AgentClassLoader加載的秩彤,又是一個(gè)全新的Cat叔扼,相當(dāng)于那些上報(bào)邏輯又初始化了一次,這這種明顯是不行的呐舔,那如何在Agent中可以使用業(yè)務(wù)加載的那個(gè)Cat對(duì)象呢币励?

后來想到了一個(gè)解決方案,通過一個(gè)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類澎现,這時(shí)拿到的Cat的class對(duì)象和業(yè)務(wù)的Cat class對(duì)象是同一個(gè)仅胞,從而避免了上述問題,在Agent內(nèi)部就可以通過CatAdapter實(shí)現(xiàn)Cat方法的代理調(diào)用剑辫,實(shí)現(xiàn)數(shù)據(jù)的埋點(diǎn)干旧。

卸載時(shí)的一些坑

為了驗(yàn)證執(zhí)行FGC時(shí),是否可以把無用的類回收妹蔽,遇到了下面這些坑椎眯。
1、很單純的以為把a(bǔ)gentLoader設(shè)置為null胳岂,我就可以快樂的回收了编整,執(zhí)行了jmap -histo:live pid之后,驚喜的發(fā)現(xiàn)乳丰,Agent的類還在掌测。
2、為了看下為什么沒有回收产园,把堆對(duì)象dump下來汞斧,通過mat工具進(jìn)行分析,找了一個(gè)Agent的類什燕,發(fā)現(xiàn)其對(duì)象正被agentLoader對(duì)象拽著粘勒,順騰摸瓜,發(fā)現(xiàn)agentLoader被線程池的線程拽著屎即,這下明白了仲义,需要把這些線程池給shutdown掉
3、因?yàn)樵贏gent初始化的時(shí)候,創(chuàng)建了幾個(gè)線程池處理一些內(nèi)部邏輯埃撵,所以要卸載Agent的時(shí)候赵颅,這些線程池必須shutdown。
4暂刘、把線程池shutdown之后饺谬,繼續(xù)使用jmap -histo:live pid,發(fā)現(xiàn)這些類特么還在谣拣,真是頑固啊募寨。dump下來,繼續(xù)分析森缠,發(fā)現(xiàn)agentLoader還被一個(gè)Finalizer對(duì)象給勾著拔鹰!這是為啥,為什么有Finalizer對(duì)象勾著它贵涵?按照我的理解列肢,只有重寫了finalize方法的類才會(huì)有Finalizer對(duì)象,一瞬間宾茂,我懷疑是不是線程池的類重寫了finalize方法瓷马,一查還真是,在ThreadPoolExecutor類中重寫了finalize方法跨晴。

5欧聘、重寫了finalize方法,這種情況理論上要經(jīng)過兩次GC才會(huì)被回收端盆,執(zhí)行了兩次jmap -histo:live pid怀骤,Agent的類果然沒了!;烂睢蒋伦!那個(gè)開心。
6访敌、后面又一次不經(jīng)意的發(fā)現(xiàn)又無法回收了凉敲,又只能dump下來衣盾,繼續(xù)分析寺旺,這次agentLoader對(duì)象被業(yè)務(wù)線程的threadLocal對(duì)象給拽著了,死都不放手势决。

這一次真的查了好久阻塑,因?yàn)椴缓脧?fù)現(xiàn),前前后后驗(yàn)證了多次果复,發(fā)現(xiàn)在使用了Agent的Mock功能之后陈莽,就會(huì)出現(xiàn)這個(gè)問題,Mock功能會(huì)根據(jù)業(yè)務(wù)配置的String字符串,通過jackson框架反序列化成一個(gè)對(duì)象并返回走搁。

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

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

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嫡丙,一起剝皮案震驚了整個(gè)濱河市拴袭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌曙博,老刑警劉巖拥刻,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異父泳,居然都是意外死亡般哼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門惠窄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒸眠,“玉大人,你說我怎么就攤上這事杆融±憧ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵脾歇,是天一觀的道長蒋腮。 經(jīng)常有香客問我,道長藕各,這世上最難降的妖魔是什么池摧? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮激况,結(jié)果婚禮上作彤,老公的妹妹穿的比我還像新娘膘魄。我一直安慰自己,他們只是感情好竭讳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布创葡。 她就那樣靜靜地躺著,像睡著了一般绢慢。 火紅的嫁衣襯著肌膚如雪蹈丸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天呐芥,我揣著相機(jī)與錄音逻杖,去河邊找鬼。 笑死思瘟,一個(gè)胖子當(dāng)著我的面吹牛荸百,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播滨攻,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼够话,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了光绕?” 一聲冷哼從身側(cè)響起女嘲,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诞帐,沒想到半個(gè)月后欣尼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡停蕉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年愕鼓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慧起。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡菇晃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蚓挤,到底是詐尸還是另有隱情磺送,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布灿意,位于F島的核電站估灿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏脾歧。R本人自食惡果不足惜甲捏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一演熟、第九天 我趴在偏房一處隱蔽的房頂上張望鞭执。 院中可真熱鬧司顿,春花似錦、人聲如沸兄纺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽估脆。三九已至钦奋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疙赠,已是汗流浹背付材。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留圃阳,地道東北人厌衔。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像捍岳,于是被迫代替她去往敵國和親富寿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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