Java Agent的隔离实现以及卸载时一些坑
轉(zhuǎn)載自? ?Java Agent的隔離實(shí)現(xiàn)以及卸載時(shí)一些坑
在《一文帶你了解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());}@Overrideprotected 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)類,避免拋出ClassNotFoundExceptionif (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ì)象勾著它?按照我的理解,只有重寫了finalize方法的類才會(huì)有Finalizer對(duì)象,一瞬間,我懷疑是不是線程池的類重寫了finalize方法,一查還真是,在 ThreadPoolExecutor類中重寫了finalize方法。
5、重寫了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);最終取消這個(gè)特性,每次序列化都去創(chuàng)建一塊內(nèi)存,這樣就可以避免這個(gè)問(wèn)題,又可以快樂(lè)的回收了。
后面還有更多的坑等著去填,越填越開(kāi)心...
總結(jié)
以上是生活随笔為你收集整理的Java Agent的隔离实现以及卸载时一些坑的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 深入理解Java ClassLoader
- 下一篇: 银行小额贷款怎么贷