在《一文帶你了解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è)問題唤反,又可以快樂的回收了。