<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          記一次 JAVA 的內(nèi)存泄露分析

          共 6517字,需瀏覽 14分鐘

           ·

          2021-07-18 15:32

          點(diǎn)擊上方“碼農(nóng)突圍”,馬上關(guān)注
          這里是碼農(nóng)充電第一站,回復(fù)“666”,獲取一份專屬大禮包
          真愛(ài),請(qǐng)?jiān)O(shè)置“星標(biāo)”或點(diǎn)個(gè)“在看
          作者:jasonGeng88
          來(lái)源:https://github.com/jasonGeng88/blog

          背景


          前不久,上線了一個(gè)新項(xiàng)目,這個(gè)項(xiàng)目是一個(gè)壓測(cè)系統(tǒng),可以簡(jiǎn)單的看做通過(guò)回放詞表(http請(qǐng)求數(shù)據(jù)),不斷地向服務(wù)發(fā)送請(qǐng)求,以達(dá)到壓測(cè)服務(wù)的目的。在測(cè)試過(guò)程中,一切還算順利,修復(fù)了幾個(gè)小bug后,就上線了。在上線后給到第一個(gè)業(yè)務(wù)方使用時(shí),就發(fā)現(xiàn)來(lái)一個(gè)嚴(yán)重的問(wèn)題,應(yīng)用大概跑了10多分鐘,就收到了大量的 Full GC 的告警。

          針對(duì)這一問(wèn)題,我們首先和業(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èn)題的。

          線上排查

          首先,我們需要在服務(wù)器上進(jìn)行排查。通過(guò) JDK 自帶的 jmap 工具,查看一下 JAVA 應(yīng)用中具體存在了哪些對(duì)象,以及其實(shí)例數(shù)和所占大小。具體命令如下:
          jmap -histo:live `pid of java`

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

          經(jīng)過(guò)觀察,確實(shí)發(fā)現(xiàn)有對(duì)象被實(shí)例化了20多萬(wàn),根據(jù)業(yè)務(wù)邏輯,實(shí)例化最多的也就是詞表,那也就10多萬(wàn),怎么會(huì)有20多萬(wàn)呢,我們?cè)诖a中也沒(méi)有找到對(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 后,我們需要通過(guò)工具進(jìn)行深入的分析。這里推薦的工具有 mat、visualVM。

          我個(gè)人比較喜歡使用 visualVM 進(jìn)行分析,它除了可以分析離線的 dump 文件,還可以與 IDEA 進(jìn)行集成,通過(guò) 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)存泄露的地方,便于我們排查。展示如下:



          場(chǎng)景再現(xiàn)

          經(jīng)過(guò)分析,最后我們定位到是使用 httpasyncclient 產(chǎn)生的內(nèi)存泄露問(wèn)題。

          httpasyncclient 是 Apache 提供的一個(gè) HTTP 的工具包,主要提供了 reactor 的 io 非阻塞模型,實(shí)現(xiàn)了異步發(fā)送 http 請(qǐng)求的功能。

          下面通過(guò)一個(gè) Demo,來(lái)簡(jiǎn)單講下具體內(nèi)存泄露的原因。

          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)建一個(gè)緩存列表,用來(lái)保存需要發(fā)送的請(qǐng)求數(shù)據(jù)。

          然后,通過(guò)循環(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<HttpUriRequest> cache1 = replay1.loadMockRequest(10000);

          //開(kāi)始循環(huán)回放
          replay1.start(cache1);

          }
          }

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


          這里以回放百度為例,創(chuàng)建10000條mock數(shù)據(jù)放入緩存列表。回放時(shí),以 while 循環(huán)每100ms 發(fā)送一個(gè)請(qǐng)求出去。具體代碼如下:   
          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)存分析:


          啟動(dòng) ReplayApplication 應(yīng)用(IDEA 中安裝 VisualVM Launcher后,可以直接啟動(dòng)visualvm),通過(guò) visualVM 進(jìn)行觀察。

          1.啟動(dòng)情況:


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



          說(shuō)明:$0代表的是對(duì)象本身,$1代表的是該對(duì)象中的第一個(gè)內(nèi)部類(lèi)。所以ReplayWithProblem$1: 代表的是ReplayWithProblem類(lèi)中FutureCallback的回調(diào)類(lèi)。

          從中,我們可以發(fā)現(xiàn) FutureCallback 類(lèi)會(huì)被不斷的創(chuàng)建。因?yàn)槊看萎惒桨l(fā)送 http 請(qǐng)求,都是通過(guò)創(chuàng)建一個(gè)回調(diào)類(lèi)來(lái)接收結(jié)果,邏輯上看上去也正常。不急,我們接著往下看。

          3.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ì)象沒(méi)有被及時(shí)的回收。

          可是該回調(diào)匿名類(lèi)在 http 回調(diào)結(jié)束后,引用關(guān)系就沒(méi)了,在下一次 GC 理應(yīng)被回收才對(duì)。我們通過(guò)對(duì) httpasyncclient 發(fā)送請(qǐng)求的源碼進(jìn)行跟蹤了一下后發(fā)現(xiàn),其內(nèi)部實(shí)現(xiàn)是將回調(diào)類(lèi)塞入到了http的請(qǐng)求類(lèi)中,而請(qǐng)求類(lèi)是放在在緩存隊(duì)列中,所以導(dǎo)致回調(diào)類(lèi)的引用關(guān)系沒(méi)有解除,大量的回調(diào)類(lèi)晉升到了old區(qū),最終導(dǎo)致 Full GC 產(chǎn)生。

          核心代碼分析:




          代碼優(yōu)化


          找到問(wèn)題的原因,我們現(xiàn)在來(lái)優(yōu)化代碼,驗(yàn)證我們的結(jié)論。因?yàn)長(zhǎng)ist<HttpUriRequest> cache1中會(huì)保存回調(diào)對(duì)象,所以我們不能緩存請(qǐng)求類(lèi),只能緩存基本數(shù)據(jù),在使用時(shí)進(jìn)行動(dòng)態(tài)的生成,來(lái)保證回調(diào)對(duì)象的及時(shí)回收。

          代碼如下:   
          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é)果驗(yàn)證


          1.啟動(dòng)情況:


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



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



          從圖中,可以證明我們得出的結(jié)論是正確的。回調(diào)類(lèi)在 Eden 區(qū)就會(huì)被及時(shí)的回收掉。old 區(qū)也沒(méi)有持續(xù)的增長(zhǎng)情況了。這一次的內(nèi)存泄露問(wèn)題算是解決了。

          總結(jié)


          關(guān)于內(nèi)存泄露問(wèn)題在第一次排查時(shí),往往是有點(diǎn)不知所措的。我們需要有正確的方法和手段,配上好用的工具,這樣在解決問(wèn)題時(shí),才能游刃有余。當(dāng)然對(duì)JAVA內(nèi)存的基礎(chǔ)知識(shí)也是必不可少的,這時(shí)你定位問(wèn)題的關(guān)鍵,不然就算工具告訴你這塊有錯(cuò),你也不能定位原因。

          最后,關(guān)于 httpasyncclient 的使用,工具本身是沒(méi)有問(wèn)題的。只是我們得了解它的使用場(chǎng)景,往往產(chǎn)生問(wèn)題多的,都是使用的不當(dāng)造成的。所以,在使用工具時(shí),對(duì)于它的了解程度,往往決定了出現(xiàn) bug 的機(jī)率。

          - END -

          最近熱文

          ?  新來(lái)的妹紙rm -rf把公司整個(gè)數(shù)據(jù)庫(kù)刪沒(méi)了,整個(gè)項(xiàng)目組慌了~
          ?  2021年7月程序員工資統(tǒng)計(jì),平均15302元
          ?  那些學(xué)計(jì)算機(jī)的女生后來(lái)都怎么樣了?
          ?  字節(jié)跳動(dòng)取消大小周,員工卻高興不起來(lái)!內(nèi)網(wǎng)哀嚎:變相降薪20%,少賺一萬(wàn)!

          瀏覽 40
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美性爱 在线 | 久热官网 | 色婷激情五月 | 久久伊大香蕉 | 久草大香蕉视频在线 |