1. 問題發(fā)生
線上突發(fā)告警,筆者負(fù)責(zé)的一個(gè)服務(wù)老年代內(nèi)存使用率到達(dá) 75% 閾值,于是立即登錄監(jiān)控系統(tǒng)查看數(shù)據(jù)。拉長時(shí)間周期,查看最近 7 天的 GC 和老年代內(nèi)存占用,監(jiān)控截圖如下。可以看到老年代占用內(nèi)存的最低點(diǎn)在逐步抬升,初步判斷是發(fā)生了內(nèi)存泄露
2. 排查過程
2.1 初步排查
從監(jiān)控上看,這個(gè)服務(wù)的兩個(gè)實(shí)例老年代內(nèi)存占用情況并不一致,其中疑似發(fā)生內(nèi)存泄露的是跑腳本的機(jī)器。于是登錄到目標(biāo)機(jī)器,首先執(zhí)行 jmap -histo 1 | head -n 100
命令查看目標(biāo)進(jìn)程的堆內(nèi)存占用前 100 的對象,發(fā)現(xiàn)其中 SkyWalking 的一個(gè) trace 追蹤對象 NoopSpan 實(shí)例總數(shù)達(dá)到了 2600 萬之巨,內(nèi)存占用也達(dá)到 600M,明顯不正常
2.2 Visual VM 內(nèi)存分析
由于生產(chǎn)環(huán)境控制嚴(yán)格,不允許在線 dump 堆內(nèi)存數(shù)據(jù),于是在預(yù)發(fā)環(huán)境執(zhí)行 jmap -dump:format=b,file=/tmp/dump 1
命令,將有相同問題的 java 進(jìn)程的堆內(nèi)存 dump 下來。下載拿到 dump 文件后,需要打開 VisualVM 加載該文件,以下為操作步驟
-
首先打開 VisualVM,點(diǎn)擊截圖中的按鈕加載 dump 文件
-
dump 文件加載后,點(diǎn)擊截圖中框出來的按鈕,切換選項(xiàng)卡為查看對象
-
由于筆者初步排查已經(jīng)確定了可疑的實(shí)例為 NoopSpan,故在對象選項(xiàng)卡界面直接過濾該對象,并展示其
相關(guān)引用、GC root
。需注意GC root
是引用鏈的起點(diǎn),從 VisualVM 的分析可以看到 NoopSpan 的實(shí)例都是以 LinkedList 節(jié)點(diǎn)的形式存在,引用鏈條為FastThreadLocalThread -> threadLocals(ThreadLocalMap) -> table(ThreadLocalMap$Entry[] 數(shù)組) -> [1](ThreadLocalMap$Entry 數(shù)組第一個(gè)元素) -> value(鍵值對 ThreadLocalMap$Entry 的值) -> activeSpanStack (SkyWalking 的 TracingContext 內(nèi)部暫存 span 的 LinkedList) -> 鏈表的一級級前后指針
,至此可以猜測是 ThradLocal 使用不當(dāng)(例如 ThreadLocal 使用后沒有remove)導(dǎo)致內(nèi)存泄露 -
確定了引用鏈,則可以看到 NoopSpan 應(yīng)該是被封裝為 LinkedList 的節(jié)點(diǎn)被保存在對象TracingContext#1 的內(nèi)部鏈表
activeSpanStack
中。此時(shí)查看該對象的鏈表的具體元素?cái)?shù)據(jù),可以看到總共有1萬多個(gè)元素,點(diǎn)開第一個(gè)節(jié)點(diǎn),查看該 LocalSpan 的名稱,確定當(dāng)前 SkyWalking 的 trace 記錄的起點(diǎn)為這個(gè) LocalSpan 的創(chuàng)建
2.3 代碼分析
-
在項(xiàng)目中搜索上一節(jié)分析出的 LocalSpan 名稱,發(fā)現(xiàn)創(chuàng)建該 Span 主要是為了在多線程環(huán)境下跨線程傳遞 trace,創(chuàng)建入口為
ContextManager#createLocalSpan()
方法。這個(gè)方法會創(chuàng)建 Trace 上下文對象 TracingContext 并將其設(shè)置到 ThreadLocal 中,創(chuàng)建出 TracingContext 對象后還會調(diào)用其相關(guān)方法創(chuàng)建 LocalSpan 對象,并將創(chuàng)建的 LocalSpan 對象存入 TracingContext 內(nèi)部的activeSpanStack
鏈表。至此基本印證了 VisualVM 的引用分析,大致確定是 ThreadLocal 的使用導(dǎo)致了內(nèi)存泄露public static AbstractSpan createLocalSpan(String operationName) { operationName = StringUtil.cut(operationName, OPERATION_NAME_THRESHOLD); AbstractTracerContext context = getOrCreate(operationName, false); return context.createLocalSpan(operationName); } private static AbstractTracerContext getOrCreate(String operationName, boolean forceSampling) { AbstractTracerContext context = CONTEXT.get(); if (context == null) { if (StringUtil.isEmpty(operationName)) { if (logger.isDebugEnable()) { logger.debug("No operation name, ignore this trace."); } context = new IgnoredTracerContext(); } else { if (EXTEND_SERVICE == null) { EXTEND_SERVICE = ServiceManager.INSTANCE.findService(ContextManagerExtendService.class); } context = EXTEND_SERVICE.createTraceContext(operationName, forceSampling); } CONTEXT.set(context); } return context; }
-
我們知道,在線程池環(huán)境下使用 ThreadLocal 如果忘記 remove 很容易發(fā)生內(nèi)存泄漏。繼續(xù)閱讀源碼,發(fā)現(xiàn) ThreadLocal 被移除的觸發(fā)點(diǎn)在
ContextManager#stopSpan()
方法,該方法每調(diào)用一次就會將之前添加到 TracingContext 內(nèi)部的activeSpanStack
鏈表中的 Span 移除,直到鏈表元素?cái)?shù)量為 0 才會去 remove 掉 ThreadLocal文章來源:http://www.zghlxwxcb.cn/news/detail-617027.htmlpublic static void stopSpan() { final AbstractTracerContext context = get(); if (Objects.isNull(context)) { return; } stopSpan(context.activeSpan(), context); } private static void stopSpan(AbstractSpan span, final AbstractTracerContext context) { try { if (context.stopSpan(span)) { CONTEXT.remove(); RUNTIME_CONTEXT.remove(); } } catch (Throwable t) { // } }
-
此時(shí)回到項(xiàng)目代碼一看,問題一目了然,代碼中創(chuàng)建了 LocalSpan 但是沒有調(diào)用相關(guān)方法把它 stop 掉,導(dǎo)致 LocalSpan 一直在 TracingContext 內(nèi)部的
activeSpanStack
鏈表中堆積,并且由于鏈表前后指針的存在無法回收,最終導(dǎo)致了內(nèi)存泄漏文章來源地址http://www.zghlxwxcb.cn/news/detail-617027.html
到了這里,關(guān)于Java 使用 VisualVM 排查內(nèi)存泄露的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!