記一次Java的記憶體泄露分析

来源:http://www.cnblogs.com/aishangJava/archive/2017/10/06/7631714.html
-Advertisement-
Play Games

當前環境 代碼地址 git 地址:https://github.com/jasonGeng88/java-network-programming 背景 前不久,上線了一個新項目,這個項目是一個壓測系統,可以簡單的看做通過回放詞表(http請求數據),不斷地向服務發送請求,以達到壓測服務的目的。在測試 ...


當前環境

  1. jdk == 1.8
  2. httpasyncclient == 4.1.3

代碼地址

git 地址:https://github.com/jasonGeng88/java-network-programming

背景

前不久,上線了一個新項目,這個項目是一個壓測系統,可以簡單的看做通過回放詞表(http請求數據),不斷地向服務發送請求,以達到壓測服務的目的。在測試過程中,一切還算順利,修複了幾個小bug後,就上線了。在上線後給到第一個業務方使用時,就發現來一個嚴重的問題,應用大概跑了10多分鐘,就收到了大量的 Full GC 的告警。

針對這一問題,我們首先和業務方確認了壓測的場景內容,回放的詞表數量大概是10萬條,回放的速率單機在 100qps 左右,按照我們之前的預估,這遠遠低於單機能承受的極限。按道理是不會產生記憶體問題的。

線上排查

首先,我們需要在伺服器上進行排查。通過 JDK 自帶的 jmap 工具,查看一下 JAVA 應用中具體存在了哪些對象,以及其實例數和所占大小。具體命令如下:

jmap -histo:live `pid of java`

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

經過觀察,確實發現有對象被實例化了20多萬,根據業務邏輯,實例化最多的也就是詞表,那也就10多萬,怎麼會有20多萬呢,我們在代碼中也沒有找到對此有顯示聲明實例化的地方。至此,我們需要對 dump 記憶體,在離線進行進一步分析,dump 命令如下:

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

離線分析

從伺服器上下載了 dump 的 heap.dump 後,我們需要通過工具進行深入的分析。這裡推薦的工具有 mat、visualVM。

我個人比較喜歡使用 visualVM 進行分析,它除了可以分析離線的 dump 文件,還可以與 IDEA 進行集成,通過 IDEA 啟動應用,進行實時的分析應用的CPU、記憶體以及GC情況(GC情況,需要在visualVM中安裝visual GC 插件)。工具具體展示如下(這裡僅僅為了展示效果,數據不是真的):

當然,mat 也是非常好用的工具,它能幫我們快速的定位到記憶體泄露的地方,便於我們排查。 展示如下:

場景再現

經過分析,最後我們定位到是使用 httpasyncclient 產生的記憶體泄露問題。httpasyncclient 是 Apache 提供的一個 HTTP 的工具包,主要提供了 reactor 的 io 非阻塞模型,實現了非同步發送 http 請求的功能。

下麵通過一個 Demo,來簡單講下具體記憶體泄露的原因。

httpasyncclient 使用介紹:

  • maven 依賴
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
    <version>4.1.3</version>
</dependency>
  • 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 的主要邏輯是這樣的,首先創建一個緩存列表,用來保存需要發送的請求數據。然後,通過迴圈的方式從緩存列表中取出需要發送的請求,將其交由 httpasyncclient 客戶端進行發送。

具體代碼如下:

public class ReplayApplication {

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

		 //創建有記憶體泄露的回放客戶端
        ReplayWithProblem replay1 = new ReplayWithProblem();
        
		 //載入一萬條請求數據放入緩存
        List<HttpUriRequest> cache1 = replay1.loadMockRequest(10000);
        
        //開始迴圈回放
        replay1.start(cache1);

    }
}

回放客戶端實現(記憶體泄露):

這裡以回放百度為例,創建10000條mock數據放入緩存列表。回放時,以 while 迴圈每100ms 發送一個請求出去。具體代碼如下:

   /**
    * Java學習交流QQ群:589809992 我們一起學Java!
    */

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);
        }
    }

}

記憶體分析:

啟動 ReplayApplication 應用(IDEA 中安裝 VisualVM Launcher後,可以直接啟動visualvm),通過 visualVM 進行觀察。

  • 啟動情況:

  • visualVM 中前後3分鐘的記憶體對象占比情況:

說明:$0代表的是對象本身,$1代表的是該對象中的第一個內部類。所以ReplayWithProblem$1: 代表的是ReplayWithProblem類中FutureCallback的回調類。

從中,我們可以發現 FutureCallback 類會被不斷的創建。因為每次非同步發送 http 請求,都是通過創建一個回調類來接收結果,邏輯上看上去也正常。不急,我們接著往下看。

  • visualVM 中前後3分鐘的GC情況:

從圖中看出,記憶體的 old 在不斷的增長,這就不對了。記憶體中維持的應該只有緩存列表的http請求體,現在在不斷的增長,就有說明瞭不斷的有對象進入old區,結合上面記憶體對象的情況,說明瞭 FutureCallback 對象沒有被及時的回收。

可是該回調匿名類在 http 回調結束後,引用關係就沒了,在下一次 GC 理應被回收才對。我們通過對 httpasyncclient 發送請求的源碼進行跟蹤了一下後發現,其內部實現是將回調類塞入到了http的請求類中,而請求類是放在在緩存隊列中,所以導致回調類的引用關係沒有解除,大量的回調類晉升到了old區,最終導致 Full GC 產生。

  • 核心代碼分析:

代碼優化

找到問題的原因,我們現在來優化代碼,驗證我們的結論。因為List<HttpUriRequest> cache1中會保存回調對象,所以我們不能緩存請求類,只能緩存基本數據,在使用時進行動態的生成,來保證回調對象的及時回收。

代碼如下:

public class ReplayApplication {

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

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

    }
}

 

 /**
  * Java學習交流QQ群:589809992 我們一起學Java!
    */

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);
        }
    }

}

結果驗證

  • 啟動情況:

  • visualVM 中前後3分鐘的記憶體對象占比情況:

  • visualVM 中前後3分鐘的GC情況:

從圖中,可以證明我們得出的結論是正確的。回調類在 Eden 區就會被及時的回收掉。old 區也沒有持續的增長情況了。這一次的記憶體泄露問題算是解決了。

總結

關於記憶體泄露問題在第一次排查時,往往是有點不知所措的。我們需要有正確的方法和手段,配上好用的工具,這樣在解決問題時,才能游刃有餘。當然對JAVA記憶體的基礎知識也是必不可少的,這時你定位問題的關鍵,不然就算工具告訴你這塊有錯,你也不能定位原因。

最後,關於 httpasyncclient 的使用,工具本身是沒有問題的。只是我們得瞭解它的使用場景,往往產生問題多的,都是使用的不當造成的。所以,在使用工具時,對於它的瞭解程度,往往決定了出現 bug 的機率。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 首先分析一下集合與數組的區別:1.java中的數組一般用於存儲基本數據類型,而且是靜態的,即長度固定不變,這就不適用於元素個數未知的情況;2.集合只能用於存儲引用類型,並且長度可變,適用於大多數情況,可用toArray()方法轉換成數組。 java語言提供了多種集合類的介面,如List、Set、Ma ...
  • 方法一: Toolkit.getDefaultToolkit().beep(); 方法二: System.out.println('\007');//八進位數 ...
  • package com.swift;//可以不要這句 import java.io.IOException; public class Shutdown100 { public static void main(String[] args) { try { Runtime.getRuntime().... ...
  • Doing Homework HDU - 1074 題意: 有n個作業,每個作業有一個截止時間和完成所需時間,如果完成某個作業的時間超出了截止時間就扣完成時間-截止時間的分。求按怎樣的順序完成作業扣分最少。 方法:狀壓dp。ans[S]表示完成集合S的作業最少的扣分(集合S用一個數字表示)。pre[ ...
  • 一、JSTL的概述 1、Apache開發與維護,依賴EL表達式 2、Apache Tomcat安裝JSTL 庫步驟如下: 從Apache的標準標簽庫中下載的二進包(jakarta-taglibs-standard-current.zip)。 官方下載地址:http://archive.apache. ...
  • Buy Tickets Problem Description Railway tickets were difficult to buy around the Lunar New Year in China, so we must get up early and join a long queu ...
  • 關於裝飾器的更多信息可以參考http://egon09.blog.51cto.com/9161406/1836763 1.裝飾器Decorator裝飾器:本質上是函數,(裝飾其他函數),就是為其他函數添加附加功能 原則:不能修改被裝飾函數的源代碼;不能修改被裝飾函數的調用方式#實現裝飾器的知識儲備:... ...
  • 歷經千辛萬苦,整理了軟體開發過程中必備英文單詞,助你走向編程巔峰 !!! 連最難的英文單詞你都征服了,你還怕什麼? (拒絕死記硬背,平時多看看,多用於代碼中,) 【不求全部都會,但求蒙的都對!】 第一天 application [ˌæplɪ'keɪʃ(ə)n]應用程式 應用、應用程式 applica ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...