摘要:展示如下場(chǎng)景再現(xiàn)經(jīng)過分析,最后我們定位到是使用產(chǎn)生的內(nèi)存泄露問題。下面通過一個(gè),來(lái)簡(jiǎn)單講下具體內(nèi)存泄露的原因。這一次的內(nèi)存泄露問題算是解決了。總結(jié)關(guān)于內(nèi)存泄露問題在第一次排查時(shí),往往是有點(diǎn)不知所措的。
記一次 JAVA 的內(nèi)存泄露分析
當(dāng)前環(huán)境摘要:本文屬于原創(chuàng),歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)保留出處:https://github.com/jasonGeng88/blog
jdk == 1.8
httpasyncclient == 4.1.3
代碼地址git 地址:https://github.com/jasonGeng88/java-network-programming
背景前不久,上線了一個(gè)新項(xiàng)目,這個(gè)項(xiàng)目是一個(gè)壓測(cè)系統(tǒng),可以簡(jiǎn)單的看做通過回放詞表(http請(qǐng)求數(shù)據(jù)),不斷地向服務(wù)發(fā)送請(qǐng)求,以達(dá)到壓測(cè)服務(wù)的目的。在測(cè)試過程中,一切還算順利,修復(fù)了幾個(gè)小bug后,就上線了。在上線后給到第一個(gè)業(yè)務(wù)方使用時(shí),就發(fā)現(xiàn)來(lái)一個(gè)嚴(yán)重的問題,應(yīng)用大概跑了10多分鐘,就收到了大量的 Full GC 的告警。
針對(duì)這一問題,我們首先和業(yè)務(wù)方確認(rèn)了壓測(cè)的場(chǎng)景內(nèi)容,回放的詞表數(shù)量大概是10萬(wàn)條,回放的速率單機(jī)在 100qps 左右,按照我們之前的預(yù)估,這遠(yuǎn)遠(yuǎn)低于單機(jī)能承受的極限。按道理是不會(huì)產(chǎn)生內(nèi)存問題的。
線上排查首先,我們需要在服務(wù)器上進(jìn)行排查。通過 JDK 自帶的 jmap 工具,查看一下 JAVA 應(yīng)用中具體存在了哪些對(duì)象,以及其實(shí)例數(shù)和所占大小。具體命令如下:
jmap -histo:live `pid of java` # 為了便于觀察,還是將輸出寫入文件 jmap -histo:live `pid of java` > /tmp/jmap00
經(jīng)過觀察,確實(shí)發(fā)現(xiàn)有對(duì)象被實(shí)例化了20多萬(wàn),根據(jù)業(yè)務(wù)邏輯,實(shí)例化最多的也就是詞表,那也就10多萬(wàn),怎么會(huì)有20多萬(wàn)呢,我們?cè)诖a中也沒有找到對(duì)此有顯示聲明實(shí)例化的地方。至此,我們需要對(duì) dump 內(nèi)存,在離線進(jìn)行進(jìn)一步分析,dump 命令如下:
jmap -dump:format=b,file=heap.dump `pid of java`離線分析
從服務(wù)器上下載了 dump 的 heap.dump 后,我們需要通過工具進(jìn)行深入的分析。這里推薦的工具有 mat、visualVM。
我個(gè)人比較喜歡使用 visualVM 進(jìn)行分析,它除了可以分析離線的 dump 文件,還可以與 IDEA 進(jìn)行集成,通過 IDEA 啟動(dòng)應(yīng)用,進(jìn)行實(shí)時(shí)的分析應(yīng)用的CPU、內(nèi)存以及GC情況(GC情況,需要在visualVM中安裝visual GC 插件)。工具具體展示如下(這里僅僅為了展示效果,數(shù)據(jù)不是真的):
當(dāng)然,mat 也是非常好用的工具,它能幫我們快速的定位到內(nèi)存泄露的地方,便于我們排查。
展示如下:
經(jīng)過分析,最后我們定位到是使用 httpasyncclient 產(chǎn)生的內(nèi)存泄露問題。httpasyncclient 是 Apache 提供的一個(gè) HTTP 的工具包,主要提供了 reactor 的 io 非阻塞模型,實(shí)現(xiàn)了異步發(fā)送 http 請(qǐng)求的功能。
下面通過一個(gè) Demo,來(lái)簡(jiǎn)單講下具體內(nèi)存泄露的原因。
httpasyncclient 使用介紹:maven 依賴
org.apache.httpcomponents httpasyncclient 4.1.3
HttpAsyncClient 客戶端
public class HttpAsyncClient { private CloseableHttpAsyncClient httpclient; public HttpAsyncClient() { httpclient = HttpAsyncClients.createDefault(); httpclient.start(); } public void execute(HttpUriRequest request, FutureCallback主要邏輯:callback){ httpclient.execute(request, callback); } public void close() throws IOException { httpclient.close(); } }
Demo 的主要邏輯是這樣的,首先創(chuàng)建一個(gè)緩存列表,用來(lái)保存需要發(fā)送的請(qǐng)求數(shù)據(jù)。然后,通過循環(huán)的方式從緩存列表中取出需要發(fā)送的請(qǐng)求,將其交由 httpasyncclient 客戶端進(jìn)行發(fā)送。
具體代碼如下:
public class ReplayApplication { public static void main(String[] args) throws InterruptedException { //創(chuàng)建有內(nèi)存泄露的回放客戶端 ReplayWithProblem replay1 = new ReplayWithProblem(); //加載一萬(wàn)條請(qǐng)求數(shù)據(jù)放入緩存 List回放客戶端實(shí)現(xiàn)(內(nèi)存泄露):cache1 = replay1.loadMockRequest(10000); //開始循環(huán)回放 replay1.start(cache1); } }
這里以回放百度為例,創(chuàng)建10000條mock數(shù)據(jù)放入緩存列表?;胤艜r(shí),以 while 循環(huán)每100ms 發(fā)送一個(gè)請(qǐng)求出去。具體代碼如下:
public class ReplayWithProblem { public List內(nèi)存分析:loadMockRequest(int n){ List cache = new ArrayList (n); for (int i = 0; i < n; i++) { HttpGet request = new HttpGet("http://www.baidu.com?a="+i); cache.add(request); } return cache; } public void start(List cache) throws InterruptedException { HttpAsyncClient httpClient = new HttpAsyncClient(); int i = 0; while (true){ final HttpUriRequest request = cache.get(i%cache.size()); httpClient.execute(request, new FutureCallback () { public void completed(final HttpResponse response) { System.out.println(request.getRequestLine() + "->" + response.getStatusLine()); } public void failed(final Exception ex) { System.out.println(request.getRequestLine() + "->" + ex); } public void cancelled() { System.out.println(request.getRequestLine() + " cancelled"); } }); i++; Thread.sleep(100); } } }
啟動(dòng) ReplayApplication 應(yīng)用(IDEA 中安裝 VisualVM Launcher后,可以直接啟動(dòng)visualvm),通過 visualVM 進(jìn)行觀察。
啟動(dòng)情況:
visualVM 中前后3分鐘的內(nèi)存對(duì)象占比情況:
說(shuō)明:$0代表的是對(duì)象本身,$1代表的是該對(duì)象中的第一個(gè)內(nèi)部類。所以ReplayWithProblem$1: 代表的是ReplayWithProblem類中FutureCallback的回調(diào)類。
從中,我們可以發(fā)現(xiàn) FutureCallback 類會(huì)被不斷的創(chuàng)建。因?yàn)槊看萎惒桨l(fā)送 http 請(qǐng)求,都是通過創(chuàng)建一個(gè)回調(diào)類來(lái)接收結(jié)果,邏輯上看上去也正常。不急,我們接著往下看。
visualVM 中前后3分鐘的GC情況:
從圖中看出,內(nèi)存的 old 在不斷的增長(zhǎng),這就不對(duì)了。內(nèi)存中維持的應(yīng)該只有緩存列表的http請(qǐng)求體,現(xiàn)在在不斷的增長(zhǎng),就有說(shuō)明了不斷的有對(duì)象進(jìn)入old區(qū),結(jié)合上面內(nèi)存對(duì)象的情況,說(shuō)明了 FutureCallback 對(duì)象沒有被及時(shí)的回收。
可是該回調(diào)匿名類在 http 回調(diào)結(jié)束后,引用關(guān)系就沒了,在下一次 GC 理應(yīng)被回收才對(duì)。我們通過對(duì) httpasyncclient 發(fā)送請(qǐng)求的源碼進(jìn)行跟蹤了一下后發(fā)現(xiàn),其內(nèi)部實(shí)現(xiàn)是將回調(diào)類塞入到了http的請(qǐng)求類中,而請(qǐng)求類是放在在緩存隊(duì)列中,所以導(dǎo)致回調(diào)類的引用關(guān)系沒有解除,大量的回調(diào)類晉升到了old區(qū),最終導(dǎo)致 Full GC 產(chǎn)生。
核心代碼分析:
代碼優(yōu)化找到問題的原因,我們現(xiàn)在來(lái)優(yōu)化代碼,驗(yàn)證我們的結(jié)論。因?yàn)?b>List
代碼如下:
public class ReplayApplication { public static void main(String[] args) throws InterruptedException { ReplayWithoutProblem replay2 = new ReplayWithoutProblem(); Listcache2 = replay2.loadMockRequest(10000); replay2.start(cache2); } }
public class ReplayWithoutProblem { public List結(jié)果驗(yàn)證loadMockRequest(int n){ List cache = new ArrayList (n); for (int i = 0; i < n; i++) { cache.add("http://www.baidu.com?a="+i); } return cache; } public void start(List cache) throws InterruptedException { HttpAsyncClient httpClient = new HttpAsyncClient(); int i = 0; while (true){ String url = cache.get(i%cache.size()); final HttpGet request = new HttpGet(url); httpClient.execute(request, new FutureCallback () { public void completed(final HttpResponse response) { System.out.println(request.getRequestLine() + "->" + response.getStatusLine()); } public void failed(final Exception ex) { System.out.println(request.getRequestLine() + "->" + ex); } public void cancelled() { System.out.println(request.getRequestLine() + " cancelled"); } }); i++; Thread.sleep(100); } } }
啟動(dòng)情況:
visualVM 中前后3分鐘的內(nèi)存對(duì)象占比情況:
visualVM 中前后3分鐘的GC情況:
從圖中,可以證明我們得出的結(jié)論是正確的。回調(diào)類在 Eden 區(qū)就會(huì)被及時(shí)的回收掉。old 區(qū)也沒有持續(xù)的增長(zhǎng)情況了。這一次的內(nèi)存泄露問題算是解決了。
總結(jié)關(guān)于內(nèi)存泄露問題在第一次排查時(shí),往往是有點(diǎn)不知所措的。我們需要有正確的方法和手段,配上好用的工具,這樣在解決問題時(shí),才能游刃有余。當(dāng)然對(duì)JAVA內(nèi)存的基礎(chǔ)知識(shí)也是必不可少的,這時(shí)你定位問題的關(guān)鍵,不然就算工具告訴你這塊有錯(cuò),你也不能定位原因。
最后,關(guān)于 httpasyncclient 的使用,工具本身是沒有問題的。只是我們得了解它的使用場(chǎng)景,往往產(chǎn)生問題多的,都是使用的不當(dāng)造成的。所以,在使用工具時(shí),對(duì)于它的了解程度,往往決定了出現(xiàn) bug 的機(jī)率。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/67681.html
摘要:首先先解讀下這個(gè)報(bào)警內(nèi)容,原因活躍線程數(shù)過多,是監(jiān)聽的端口號(hào)用來(lái)獲取虛擬機(jī)各項(xiàng)信息,代表著此時(shí)的線程數(shù),是設(shè)置的報(bào)警閾值。 前言 前天,一位21世紀(jì)的好好青年正在工位上默念社會(huì)主義大法好的時(shí)候,釘釘上又報(bào)警了(公司項(xiàng)目接入了open-faclon監(jiān)控,指標(biāo)不正常會(huì)報(bào)警給釘釘?shù)臋C(jī)器人),無(wú)奈默默流淚揮手告別社會(huì)主義大法開始定位線上問題。 報(bào)警內(nèi)容 首先我們先來(lái)看下報(bào)警信息,為防止泄露公...
摘要:現(xiàn)象登入生產(chǎn)環(huán)境,使用命令因?yàn)檫@時(shí)候并沒有打的,所以只能觀察現(xiàn)象。其他的可以根據(jù)這個(gè)類推,是內(nèi)純的占用量。 前言 我們的游戲上線之初,經(jīng)常有玩家反饋卡,或者有網(wǎng)絡(luò)延遲等現(xiàn)象,造成用戶流失等現(xiàn)象,這時(shí)候我就想到是不是可能是之前的jvm配置有問題,或者存在內(nèi)存泄露等問題。 現(xiàn)象 登入生產(chǎn)環(huán)境,使用命令,因?yàn)檫@時(shí)候并沒有打gc的log,所以只能觀察現(xiàn)象。 jstat -gcutil 270...
摘要:直接顯示了一個(gè)疑似內(nèi)存泄漏的問題。然后分析文件給出的信息,發(fā)現(xiàn)一個(gè)叫的類。文件里面說(shuō)的內(nèi)存泄漏的大概的意思就是說(shuō),這個(gè)類里面的存放的東西太多了,爆掉了。修改了代碼將調(diào)用的地方改成了單例。修改完線上跑了一段日子,后來(lái)也沒有出現(xiàn)過這樣的問題。 問題描述: ????早上去公司上班,突然就郵件一直報(bào)警,接口報(bào)異常,然后去查服務(wù)器的運(yùn)行情況,發(fā)現(xiàn)java的cpu爆了.接著就開始排查問題 問題解決...
閱讀 3546·2023-04-25 20:09
閱讀 3745·2022-06-28 19:00
閱讀 3066·2022-06-28 19:00
閱讀 3092·2022-06-28 19:00
閱讀 3185·2022-06-28 19:00
閱讀 2886·2022-06-28 19:00
閱讀 3057·2022-06-28 19:00
閱讀 2643·2022-06-28 19:00