ThreadLocal導(dǎo)致內(nèi)存泄漏排查小記
背景描述
公司sso域名變動(dòng),所有涉及的產(chǎn)品都要修改相關(guān)的配置。配置修改好之后,運(yùn)行期間發(fā)現(xiàn)業(yè)務(wù)系統(tǒng)不穩(wěn)定,出現(xiàn)了很多json解析異常。但是隨著sso那邊問題得到修改,我們自己的產(chǎn)品也逐漸穩(wěn)定起來(lái),但查看日志發(fā)現(xiàn)多條內(nèi)存泄露的日志,于是本著學(xué)習(xí)的心態(tài),對(duì)具體的原因進(jìn)行了粗略的分析,最終得出的結(jié)論是異常導(dǎo)致threadLocal.remove()方法沒有執(zhí)行,最后內(nèi)存泄漏了,以下是本人定位問題的過(guò)程。
報(bào)錯(cuò)日志
6:21:26.656 嚴(yán)重 [Thread-219] org.apache.catalina.loader.WebappClassLoaderBase.checkThreadLocalMapForLeaksThe web application [ttt] created a ThreadLocal withkey of type [java.lang.ThreadLocal](value [java.lang.ThreadLocal@1201c9a0]) and a value of type[tt.zzz.loghelper.model.ActionLog] (value [])but failed to remove it when the web application was stopped.Threads are going to be renewed over time to try and avoid a probable memory leak.
翻譯和分析
這個(gè)threadlocal移除不了,直到項(xiàng)目死了都還沒移除掉。具體的異常發(fā)起者是這個(gè)catalina的loader,具體的方法就是checkThreadLocalMapForLeaks (檢測(cè)線程的threadlocal是否有泄露),大概說(shuō)一下就是就是說(shuō)檢測(cè)這個(gè)線程的threadlocal,然后發(fā)現(xiàn)線程中的threadlocal有值,然后就拋出了內(nèi)存泄露這個(gè)異常。大概猜測(cè)一下應(yīng)該是是tomcat在處理請(qǐng)求的時(shí)候,因?yàn)橐獜木€程池中獲取線程,然后讓這個(gè)線程去跑請(qǐng)求,但是通過(guò)這個(gè)檢測(cè)方法檢測(cè)一下,發(fā)現(xiàn)當(dāng)前獲取的這個(gè)線程的threadLocal沒有釋放掉。我們當(dāng)時(shí)說(shuō)threadlocal是一個(gè)弱引用,我們說(shuō)弱引用只會(huì)在內(nèi)存不夠的時(shí)候,jvm才會(huì)回收它。而我們的thredlocal保存的map映射關(guān)系就是保存在這里的弱引用中,意思是如果我們不顯式的通過(guò)remove()方法去移除弱引用中的值,那么就會(huì)存在內(nèi)存泄露的問題。所以這個(gè)報(bào)錯(cuò)日志的核心就是沒有走threadlocal.remove()方法。

定位問題代碼
日志收集上,我們采用了之前老員工寫的日志切面,大概得代碼如下:
public class WebLogAspect {private static final Logger log = LoggerFactory.getLogger(WebLogAspect.class);private ThreadLocalthreadLocal = new ThreadLocal();private LogHelperProperties logHelperProperties;private LogService logService;public WebLogAspect() {}("@annotation(ttt .tt.loghelper.aspect.WebLog)")public void webLog() {}//執(zhí)行之前("webLog()")public void doBefore(JoinPoint joinPoint) {ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if (attributes != null) {HttpServletRequest request = attributes.getRequest();ActionLog actionLog = new ActionLog();//設(shè)置threadLocal變量this.threadLocal.set(actionLog);}}//這里回環(huán)日志("webLog()")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//這里繼續(xù)執(zhí)行我們自己的函數(shù)Object result = proceedingJoinPoint.proceed();ActionLog actionLog = (ActionLog)this.threadLocal.get();//這里對(duì)threadLocal進(jìn)行remove操作,這里應(yīng)該沒有執(zhí)行?this.threadLocal.remove();//寫日志this.logService.writeActionLog(actionLog);return result;}}
通過(guò)查看代碼我們知道這塊的 this.threadLocal.remove();應(yīng)該是沒有執(zhí)行的,那么沒有執(zhí)行的原因就是異常了。為此作者編寫了如下的代碼測(cè)試了一下:
public class TestThread extends Thread{private static ThreadLocalmyThreadThreadLocal=new ThreadLocal<>();public static void main(String[] args) {MyThread thread=new MyThread();thread.setName("tianjl");//設(shè)置threadLocal變量myThreadThreadLocal.set(thread);try{//里邊拋異常doSomeThing();//下邊的代碼是不執(zhí)行的,也就是this.threadLocal.remove();不執(zhí)行System.out.println(myThreadThreadLocal.get().toString());myThreadThreadLocal.remove();}catch (Exception e){e.printStackTrace();}//這里可以獲取到本該remove的threadlocal的值System.out.println(myThreadThreadLocal.get().toString());}private static void doSomeThing() throws Exception {throw new Exception("測(cè)試異常");}}
執(zhí)行的效果如下

結(jié)論和解決方法
根據(jù)SSO的變動(dòng)我們知道,sso異常導(dǎo)致了線程直接跳出方法,使得函數(shù)沒有執(zhí)行threadlocal.remove()方法。造成了threadlocal中的值沒有清理,最終導(dǎo)致tomcat在檢測(cè)線程的threadlocal的時(shí)候發(fā)現(xiàn)有內(nèi)存泄露,最后直接拋異常了。具體的解決方法就是將threadlocal.remove()放到finally中去。
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {try{//這里繼續(xù)執(zhí)行我們自己的函數(shù)Object result = proceedingJoinPoint.proceed();ActionLog actionLog = (ActionLog)this.threadLocal.get();//寫日志this.logService.writeActionLog(actionLog);return result;}finally{//這里對(duì)threadLocal進(jìn)行remove操作,這里應(yīng)該沒有執(zhí)行?this.threadLocal.remove();}}
