java內(nèi)存泄漏分析

0厅瞎、背景

前不久饰潜,上線了一個新項目,這個項目是一個壓測系統(tǒng)和簸,可以簡單的看做通過回放詞表(http請求數(shù)據(jù))彭雾,不斷地向服務(wù)發(fā)送請求锁保,以達到壓測服務(wù)的目的薯酝。在測試過程中半沽,一切還算順利,修復(fù)了幾個小bug后吴菠,就上線了者填。在上線后給到第一個業(yè)務(wù)方使用時,就發(fā)現(xiàn)來一個嚴重的問題做葵,應(yīng)用大概跑了10多分鐘占哟,就收到了大量的 Full GC 的告警。
針對這一問題酿矢,我們首先和業(yè)務(wù)方確認了壓測的場景內(nèi)容榨乎,回放的詞表數(shù)量大概是10萬條,回放的速率單機在 100qps 左右瘫筐,按照我們之前的預(yù)估蜜暑,這遠遠低于單機能承受的極限。按道理是不會產(chǎn)生內(nèi)存問題的策肝。

1肛捍、線上排查
首先,我們需要在服務(wù)器上進行排查之众。通過 JDK 自帶的 jmap 工具拙毫,查看一下 JAVA 應(yīng)用中具體存在了哪些對象,以及其實例數(shù)和所占大小酝枢。具體命令如下:

jmap -histo:live `pid of java`

# 為了便于觀察恬偷,還是將輸出寫入文件
jmap -histo:live `pid of java` > /tmp/jmap00

經(jīng)過觀察悍手,確實發(fā)現(xiàn)有對象被實例化了20多萬帘睦,根據(jù)業(yè)務(wù)邏輯,實例化最多的也就是詞表坦康,那也就10多萬竣付,怎么會有20多萬呢,我們在代碼中也沒有找到對此有顯示聲明實例化的地方滞欠。至此古胆,我們需要對 dump 內(nèi)存,在離線進行進一步分析筛璧,dump 命令如下:

jmap -dump:format=b,file=heap.dump `pid of java

2逸绎、離線分析

從服務(wù)器上下載了 dump 的 heap.dump 后,我們需要通過工具進行深入的分析夭谤。這里推薦的工具有 mat棺牧、visualVM。

我個人比較喜歡使用 visualVM 進行分析朗儒,它除了可以分析離線的 dump 文件颊乘,還可以與 IDEA 進行集成参淹,通過 IDEA 啟動應(yīng)用,進行實時的分析應(yīng)用的CPU乏悄、內(nèi)存以及GC情況(GC情況浙值,需要在visualVM中安裝visual GC 插件)。工具具體展示如下(這里僅僅為了展示效果檩小,數(shù)據(jù)不是真的):

image
image

當然开呐,mat 也是非常好用的工具,它能幫我們快速的定位到內(nèi)存泄露的地方规求,便于我們排查负蚊。展示如下:

image
image

3、場景再現(xiàn):經(jīng)過分析颓哮,最后我們定位到是使用 httpasyncclient 產(chǎn)生的內(nèi)存泄露問題家妆。httpasyncclient 是 Apache 提供的一個 HTTP 的工具包,主要提供了 reactor 的 io 非阻塞模型冕茅,實現(xiàn)了異步發(fā)送 http 請求的功能伤极。下面通過一個 Demo,來簡單講下具體內(nèi)存泄露的原因姨伤。

4哨坪、httpasyncclient 使用介紹:

1.maven 依賴

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
    <version>4.1.3</version>
</dependency>

2.HttpAsyncClient 客戶端

public class HttpAsyncClient {

    private CloseableHttpAsyncClient httpclient;

    public HttpAsyncClient() {
        httpclient = HttpAsyncClients.createDefault();
        httpclient.start();
    }

    public void execute(HttpUriRequest request, FutureCallback<HttpResponse> callback){
        httpclient.execute(request, callback);
    }

    public void close() throws IOException {
        httpclient.close();
    }

}

主要邏輯:

Demo 的主要邏輯是這樣的,首先創(chuàng)建一個緩存列表乍楚,用來保存需要發(fā)送的請求數(shù)據(jù)当编。

然后,通過循環(huán)的方式從緩存列表中取出需要發(fā)送的請求徒溪,將其交由 httpasyncclient 客戶端進行發(fā)送忿偷。

具體代碼如下:

public class ReplayApplication {

    public static void main(String[] args) throws InterruptedException {

     //創(chuàng)建有內(nèi)存泄露的回放客戶端
        ReplayWithProblem replay1 = new ReplayWithProblem();
        
     //加載一萬條請求數(shù)據(jù)放入緩存
        List<HttpUriRequest> cache1 = replay1.loadMockRequest(10000);
        
        //開始循環(huán)回放
        replay1.start(cache1);

    }
}

回放客戶端實現(xiàn)(內(nèi)存泄露):

這里以回放百度為例,創(chuàng)建10000條mock數(shù)據(jù)放入緩存列表臊泌±鹎牛回放時,以 while 循環(huán)每100ms 發(fā)送一個請求出去渠概。具體代碼如下:

public class ReplayWithProblem {

    public List<HttpUriRequest> loadMockRequest(int n){
    
        List<HttpUriRequest> cache = new ArrayList<HttpUriRequest>(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<HttpUriRequest> 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<HttpResponse>() {
                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);
        }
    }

}

內(nèi)存分析:

啟動 ReplayApplication 應(yīng)用(IDEA 中安裝 VisualVM Launcher后茶凳,可以直接啟動visualvm),通過 visualVM 進行觀察播揪。

1.啟動情況:

image

2.visualVM 中前后3分鐘的內(nèi)存對象占比情況:

image
image

說明:0代表的是對象本身贮喧,1代表的是該對象中的第一個內(nèi)部類。所以ReplayWithProblem$1: 代表的是ReplayWithProblem類中FutureCallback的回調(diào)類猪狈。從中箱沦,我們可以發(fā)現(xiàn) FutureCallback 類會被不斷的創(chuàng)建。因為每次異步發(fā)送 http 請求罪裹,都是通過創(chuàng)建一個回調(diào)類來接收結(jié)果饱普,邏輯上看上去也正常运挫。不急,我們接著往下看套耕。

3.visualVM 中前后3分鐘的GC情況:

image
image

從圖中看出谁帕,內(nèi)存的 old 在不斷的增長,這就不對了冯袍。內(nèi)存中維持的應(yīng)該只有緩存列表的http請求體匈挖,現(xiàn)在在不斷的增長,就有說明了不斷的有對象進入old區(qū)康愤,結(jié)合上面內(nèi)存對象的情況儡循,說明了 FutureCallback 對象沒有被及時的回收。歡迎關(guān)注公眾號"Java學習之道"征冷,查看更多干貨择膝!可是該回調(diào)匿名類在 http 回調(diào)結(jié)束后,引用關(guān)系就沒了检激,在下一次 GC 理應(yīng)被回收才對肴捉。我們通過對 httpasyncclient 發(fā)送請求的源碼進行跟蹤了一下后發(fā)現(xiàn),其內(nèi)部實現(xiàn)是將回調(diào)類塞入到了http的請求類中叔收,而請求類是放在在緩存隊列中齿穗,所以導致回調(diào)類的引用關(guān)系沒有解除,大量的回調(diào)類晉升到了old區(qū)饺律,最終導致 Full GC 產(chǎn)生窃页。

核心代碼分析:

image

image
image

代碼優(yōu)化:

找到問題的原因,我們現(xiàn)在來優(yōu)化代碼复濒,驗證我們的結(jié)論脖卖。因為List<HttpUriRequest> cache1中會保存回調(diào)對象,所以我們不能緩存請求類芝薇,只能緩存基本數(shù)據(jù)胚嘲,在使用時進行動態(tài)的生成作儿,來保證回調(diào)對象的及時回收洛二。

代碼如下:

public class ReplayApplication {

    public static void main(String[] args) throws InterruptedException {

        ReplayWithoutProblem replay2 = new ReplayWithoutProblem();
        List<String> cache2 = replay2.loadMockRequest(10000);
        replay2.start(cache2);

    }
}
public class ReplayWithoutProblem {

    public List<String> loadMockRequest(int n){
        List<String> cache = new ArrayList<String>(n);
        for (int i = 0; i < n; i++) {
            cache.add("http://www.baidu.com?a="+i);
        }
        return cache;
    }

    public void start(List<String> 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<HttpResponse>() {
                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);
        }
    }

}

結(jié)果驗證

1.啟動情況:


image

2.visualVM 中前后3分鐘的內(nèi)存對象占比情況:

image
image

3.visualVM 中前后3分鐘的GC情況:

image
image

從圖中,可以證明我們得出的結(jié)論是正確的攻锰×浪唬回調(diào)類在 Eden 區(qū)就會被及時的回收掉。old 區(qū)也沒有持續(xù)的增長情況了娶吞。這一次的內(nèi)存泄露問題算是解決了垒迂。

5、總結(jié)

關(guān)于內(nèi)存泄露問題在第一次排查時妒蛇,往往是有點不知所措的机断。我們需要有正確的方法和手段楷拳,配上好用的工具,這樣在解決問題時吏奸,才能游刃有余欢揖。當然對JAVA內(nèi)存的基礎(chǔ)知識也是必不可少的,這時你定位問題的關(guān)鍵奋蔚,不然就算工具告訴你這塊有錯她混,你也不能定位原因。最后泊碑,關(guān)于 httpasyncclient 的使用坤按,工具本身是沒有問題的。只是我們得了解它的使用場景馒过,往往產(chǎn)生問題多的臭脓,都是使用的不當造成的。所以腹忽,在使用工具時谢鹊,對于它的了解程度,往往決定了出現(xiàn) bug 的機率留凭。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末佃扼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蔼夜,更是在濱河造成了極大的恐慌兼耀,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件求冷,死亡現(xiàn)場離奇詭異瘤运,居然都是意外死亡,警方通過查閱死者的電腦和手機匠题,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門拯坟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人韭山,你說我怎么就攤上這事郁季。” “怎么了钱磅?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵梦裂,是天一觀的道長。 經(jīng)常有香客問我盖淡,道長年柠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任褪迟,我火速辦了婚禮冗恨,結(jié)果婚禮上答憔,老公的妹妹穿的比我還像新娘。我一直安慰自己掀抹,他們只是感情好攀唯,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渴丸,像睡著了一般侯嘀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谱轨,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天戒幔,我揣著相機與錄音,去河邊找鬼土童。 笑死诗茎,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的献汗。 我是一名探鬼主播敢订,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼罢吃!你這毒婦竟也來了楚午?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤尿招,失蹤者是張志新(化名)和其女友劉穎矾柜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體就谜,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡怪蔑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了丧荐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缆瓣。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖虹统,靈堂內(nèi)的尸體忽然破棺而出弓坞,到底是詐尸還是另有隱情,我是刑警寧澤窟却,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布昼丑,位于F島的核電站,受9級特大地震影響夸赫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咖城,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一茬腿、第九天 我趴在偏房一處隱蔽的房頂上張望呼奢。 院中可真熱鬧,春花似錦切平、人聲如沸握础。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽禀综。三九已至,卻和暖如春苔严,著一層夾襖步出監(jiān)牢的瞬間定枷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工届氢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留欠窒,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓退子,卻偏偏與公主長得像岖妄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子寂祥,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容