在《一文帶你了解Java Agent》中,讓大家了解了Java Agent的來(lái)龍去脈,當(dāng)通過(guò)attach方式去動(dòng)態(tài)加載一個(gè)Java Agent時(shí),Agent中的類會(huì)被加載到業(yè)務(wù)的虛擬機(jī)中,在使用完Agent的之后,如果想卸載這些無(wú)用的類,怎么實(shí)現(xiàn)?
這里就涉及到如何回收Perm區(qū)、或者M(jìn)etaspace中已經(jīng)加載的類了,如果一個(gè)類的類加載器對(duì)象沒(méi)有GC Root關(guān)聯(lián),那么可以通過(guò)FGC的方式回收這些類。不過(guò),如果通過(guò)JVM內(nèi)部的類加載器比如AppClassLoader去加載這些類的話,可能永遠(yuǎn)也不能回收了,所以得通過(guò)自定義的類加載器去實(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);
}
}
這樣,通過(guò)AgentClassLoader加載的類,就可以和業(yè)務(wù)的類完全隔離開(kāi),在需要回收這些類的時(shí)候,只要把AgentClassLoader對(duì)象和GC root的關(guān)聯(lián)完全掐斷就行。
不過(guò)用了AgentClassLoader之后,還是遇到了一些坑,比如在Agent中使用Cat的時(shí)候,因?yàn)镃at是單例模式,都是通過(guò)Cat.logEvent這種方式使用,所以在第一次使用Cat的時(shí)候,Cat內(nèi)部會(huì)進(jìn)行初始化,比如系統(tǒng)信息上報(bào)邏輯。因?yàn)闃I(yè)務(wù)邏輯在使用Cat的時(shí)候,已經(jīng)初始化過(guò)了一次,在Agent內(nèi)部使用時(shí),因?yàn)槭峭ㄟ^(guò)AgentClassLoader加載的,又是一個(gè)全新的Cat,相當(dāng)于那些上報(bào)邏輯又初始化了一次,這這種明顯是不行的,那如何在Agent中可以使用業(yè)務(wù)加載的那個(gè)Cat對(duì)象呢?
后來(lái)想到了一個(gè)解決方案,通過(guò)一個(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);
又通過(guò)agentLoader去加載CatAdapter類,在init方法中,通過(guò)當(dāng)前線程的classLoader去加載真正的Cat類,這時(shí)拿到的Cat的class對(duì)象和業(yè)務(wù)的Cat class對(duì)象是同一個(gè),從而避免了上述問(wèn)題,在Agent內(nèi)部就可以通過(guò)CatAdapter實(shí)現(xiàn)Cat方法的代理調(diào)用,實(shí)現(xiàn)數(shù)據(jù)的埋點(diǎn)。
卸載時(shí)的一些坑
為了驗(yàn)證執(zhí)行FGC時(shí),是否可以把無(wú)用的類回收,遇到了下面這些坑。
1、很單純的以為把a(bǔ)gentLoader設(shè)置為null,我就可以快樂(lè)的回收了,執(zhí)行了jmap -histo:live pid之后,驚喜的發(fā)現(xiàn),Agent的類還在。
2、為了看下為什么沒(méi)有回收,把堆對(duì)象dump下來(lái),通過(guò)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下來(lái),繼續(xù)分析,發(fā)現(xiàn)agentLoader還被一個(gè)Finalizer對(duì)象給勾著!這是為啥,為什么有Finalizer對(duì)象勾著它?按照我的理解,只有重寫(xiě)了finalize方法的類才會(huì)有Finalizer對(duì)象,一瞬間,我懷疑是不是線程池的類重寫(xiě)了finalize方法,一查還真是,在ThreadPoolExecutor類中重寫(xiě)了finalize方法。

5、重寫(xiě)了finalize方法,這種情況理論上要經(jīng)過(guò)兩次GC才會(huì)被回收,執(zhí)行了兩次jmap -histo:live pid,Agent的類果然沒(méi)了?。?!那個(gè)開(kāi)心。
6、后面又一次不經(jīng)意的發(fā)現(xiàn)又無(wú)法回收了,又只能dump下來(lái),繼續(xù)分析,這次agentLoader對(duì)象被業(yè)務(wù)線程的threadLocal對(duì)象給拽著了,死都不放手。
這一次真的查了好久,因?yàn)椴缓脧?fù)現(xiàn),前前后后驗(yàn)證了多次,發(fā)現(xiàn)在使用了Agent的Mock功能之后,就會(huì)出現(xiàn)這個(gè)問(wèn)題,Mock功能會(huì)根據(jù)業(yè)務(wù)配置的String字符串,通過(guò)jackson框架反序列化成一個(gè)對(duì)象并返回。
jackson在序列化的時(shí)候,需要開(kāi)辟一塊內(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);
最終通過(guò)取消這個(gè)特性,每次序列化都去創(chuàng)建一塊內(nèi)存,這樣就可以避免這個(gè)問(wèn)題,又可以快樂(lè)的回收了。