一次使用MAT進行線上內存泄漏問題排查經歷

一、背景

首先,發現線上某分析應用出現異常,連續好幾天,一直沒有分析數據產出。故登陸到線上查看error.log日誌,發現:
在這裏插入圖片描述
明顯是 YCYX-Task 這個線程出現了內存溢出,導致程序假死。

二、排查歷程

1、初步定位

jinfo

首先,我們使用jinfo pid查看當前jvm的堆相關參數:
在這裏插入圖片描述
可見,最大堆容量爲:4G。

jstat

接下來,我們使用命令jstat -gcutil pid 1s 5查看5秒內當前堆佔用情況:
在這裏插入圖片描述
如上,新生代已經滿了(佔用97.33%),老年代也已經滿了(佔用100%),同時FullGC高達967次!

jmap

除了jstat命令外,我們也可以使用jmap -heap pid查看下當前JVM堆情況:
在這裏插入圖片描述
然後,我們用jmap -F -histo pid | head -n 13查看前13行打印,即查看TOP10的大對象(最好用head限制一下,否則列出的對象會鋪滿你的屏幕,另外:強制連接參數-Fjmap -histo:live是無效的):
在這裏插入圖片描述
如上,可以看到,除了幾大基本類型外(因爲各對象的底層都是幾個基本類型,所以無意外它們會排在top前幾),一個java.util.HashMapjava.util.ArrayList非常顯眼……先記下,後面繼續分析先。

最後,我們使用命令jmap -F -dump:file=a.bin pid將堆dump出來,發現dump出來的文件有4.02G,很恐怖,故使用tar -czvf a.tar.gz a.bin打包壓縮一下!

2、MAT深入分析

調整MAT最大堆內存

將打包好的a.tar.gz拉回到本地,並解壓。但是由於a.bin過大,MAT打開肯定會內存溢出,故調整MAT軟件的最大堆內存:

[ MAT根目錄下的MemoryAnalyzer.ini ]
-startup
plugins/org.eclipse.equinox.launcher_1.5.0.v20180512-1130.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.700.v20180518-1200
-vmargs
-Xmx6g

-Xmx 改爲6G!

MAT分析大對象

在這裏插入圖片描述
映入眼簾的就是一個超、超、超級大的對象,3GB,佔用了97.25的內存,且位於 YCYX-Task 這個線程內,印證了開頭的error.log報錯日誌YCYX-Task報內存泄漏的情況。然後點開這個java.lang.Object[]Details,如下圖:
在這裏插入圖片描述
可以看到,這個Object數組,的確佔用了3個G的內存,同時也的確在一個ArrayList內部,印證了剛剛jmap -histo pid | head -n 13一個異常ArrayList的情況,同時它內部也正是由HashMap構成!
在這裏插入圖片描述
圖上,可以看到這個ArrayList存了接近31萬個元素,故導致內存泄漏。最終得出結論是由於代碼裏面的一個ArrayList問題!

代碼走讀排查

結合error.log日誌報出的問題JAVA類報錯代碼行數,再結合問題應該出在一個ArrayList上,很容易就定位到了相關問題代碼塊:

  /**
	* 按照指定起止時間分析數據
	* @param beginTime 起始時間
	* @param endTime 截至時間
	*/
   @Override public void exec(String beginTime, String endTime) {
        List<Map<String, Object>> emlWithLoginList = new ArrayList<>();
        List<Map<String, Object>> emlList = new ArrayList<>();

        while (true) {
            try {
				//如果已分析到截至時間,則退出。
            	if (DateUtil.compareTime(beginTime, endTime) > 0) {
                    break;
                }

                //每次循環向前推10小時,YCYX_PERIOD=10小時
                String tmpTime=DateUtil.addHours(beginTime, YCYX_PERIOD);
                
                //1.構造請求
                BoolQueryBuilder bqb = QueryBuilders.boolQuery();
                bqb.must(QueryBuilders.rangeQuery(CREATE_TIME).gt(beginTime).lte(tmpTime));
                bqb.must(QueryBuilders.termQuery(IS_DELETE, IS_FIELD_VALUE));
                bqb.must(QueryBuilders.existsQuery(TOS));
                bqb.must(QueryBuilders.existsQuery(FROMS));
                bqb.must(QueryBuilders.existsQuery(SEND_TYPE));
                bqb.must(QueryBuilders.existsQuery(SESSION_ID));

                log.debug("emlAnalysis begin at " + beginTime + ", and end at " + lastTime);

                SearchSourceBuilder requestBuilder = new SearchSourceBuilder().query(bqb).size(PAGE_SIZE).sort(CREATE_TIME, SortOrder.ASC).fetchSource(new String[] {"*"},
                        new String[] {EML_CONTENT});

                //2.發起請求
                EsHelper.getResponseScroll(EsCluster.DEFAULT, INF_EML_INDEX, "14m", requestBuilder.toString(), result -> {
                            
                            //3.解析結果
                            EsSearchHit[] hits = result.getHits().getHits();
                            List<Map<String, Object>> loginList;
                            for (EsSearchHit hit : hits) {
                            	//將郵件Map添加到列表中
                             	Map<String, Object> emlMap = hit.getSource();
                                emlList.add(emlMap);
                                
                                //將郵件+MailLogin的Map添加到另一個列表中
                                Map<String, Object> emlWithLoginMap = new HashMap(emlMap);               
                                String sessionId = emlMap.get(SESSION_ID).toString();
                                MailLogin mailLogin = EsService.getMailLoginBySessionId(sessionId);
                                emlWithLoginMap.put("Login_Key", mailLogin );
                                emlWithLoginList.add(emlWithLoginMap);
                            }
                            return true;
                        });

                //4.A類檢測邏輯
                checkDSYJEml(emlWithLoginList);
				emlWithLoginList.clear();

                //5.B類檢測邏輯
                checkYCYXEml(emlList);
				emlList.clear();
                
                beginTime = tmpTime;
            } catch (Throwable e) {
                log.error("", e);
            }
        }
    }

由上面代碼可見,問題List應該就是 emlWithLoginList 或者 emlList的其中一個了,而兩個List存儲的內容基本相同,除了emlWithLoginList內的Map元素額外存了一個叫做Login_Key的key!

而通過MAT查看了問題List內Map元素的所有Key,並沒有找到相關叫做Login_Key的元素,故推測問題List應該是這個emlList
在這裏插入圖片描述
而直接看上面的代碼邏輯,沒有發現什麼大的問題。且本方法是按照傳入的起、止時間,按每10小時爲時間段,依次進行ES數據查詢、分析的。

因此我們先猜測,是否是因爲當前時間段的ES數據過大導致?

代碼裏面可以看到,ES查詢的數據,是通過CREATE_TIME(該常量值爲@createtime)進行升序查詢的,故先查的數據,應該位於這個問題List的開頭,而後查的數據在問題List的結尾。

因此通過MAT找到問題List的第一個和最後一個元素,即得到本次查詢的起、止時間:
在這裏插入圖片描述
在這裏插入圖片描述
可以看到,時間段大致爲2019-11-01 16:00:00到2019-11-02 02:00:00!時間符合我們的每批數據查詢的10小時時間段。

圖上說明,查詢這一批數據,程序得到了31萬條數據!

而我又到Kibana查詢了一下這個時間段的數據量:
在這裏插入圖片描述
才兩萬多條,完全對不上啊?

一下子就迷了,這個情況有點說不通,想了半天,於是,又仔細地看了下代碼:
在這裏插入圖片描述
注意到了圖上位置的代碼,這兩個List都在不斷的添加元素,然後執行各自的檢測邏輯,最後調用clear()方法清空List,爲何emlWithLoginList沒有問題,而emlList卻內存溢出了?

那麼就很顯然了:

  1. 假如在執行checkYCYXEml(emlList)時,出現了異常,就會直接被下面的catch給捕獲;
  2. 因而不會走下面的emlList.clear()代碼,同時也不會走beginTime = timeTime
  3. 由於try catch位於while循環內部,因此拋出異常後會繼續循環,且因爲沒有執行beginTime = timeTime,故查詢的數據還是之前的這個時間段的數據;
  4. 同時,也可以解釋爲什麼emlWithLoginList沒有問題了,因爲在異常代碼的前面,可以進行正常的clear()操作。

那麼,如果是checkYCYXEml(emlList)時,出現了異常,error.log應該是有異常日誌打印的,通過關鍵字checkYCYXEml搜了一下:
在這裏插入圖片描述
果然找到了這個方法報出的異常,並且原因是Ice超時。這個我是知道的,由於公司新上的EsService中間件(部署在分佈式RPC框架-Ice上面的),限制了各調用方的ES查詢時間,超過指定時間,會強行返回一個Ice超時錯誤,目的是爲了防止不規範的複雜語句把ES查崩!

按照上面說的,如果是這個原因,這段代碼會重複查詢2019-11-01 16:00:002019-11-02 02:00:00的數據,且不斷加入到emlList中,最後撐爆JVM!

那麼MAT中的這個問題List應該會有多個相同元素存在(數據重複加入進去了嘛)。

如何驗證這一點呢?

因爲這些數據有一個emlkey字段,是一個唯一主鍵,對應這條記錄在ES中的_id,因此可通過MAT,根據某一條數據的emlkey,去查找是否問題emlList中有多個元素均存在相同的emlkey,即可證明。

MAT對字符串進行分組

在這裏插入圖片描述
在這裏插入圖片描述
由於這個dump文件很大,故查詢需要花很長時間,需耐心等待~
在這裏插入圖片描述
題外話:我們可以看到,很多String值是相同的,但卻分配了好幾十萬個String對象來存儲,這裏我們可以使用Java 8的-XX:+UseStringDeduplication功能,來減輕重複字符串的問題。這將導致這幾十萬個String實例,但其底層的數組均指向同一個char數組。

圖上即是對針對String進行的分組操作,這時,我們隨便找一個元素的emlkey,查詢一下:
在這裏插入圖片描述
然後選中該記錄,右鍵,使用merge shortest paths to gc roots功能,可查看這些對象到GC ROOT的最短路徑,說白了,就是想通過這個功能,查看當前這個String是屬於哪個對象下面的:
在這裏插入圖片描述
可以看到,有32個String對象,值均爲854742740486326718e
在這裏插入圖片描述
在這裏插入圖片描述
那麼爲什麼有32個String呢,這是因爲每個對象,除了有一個emlkey屬性,還有一個document_id屬性,這兩個值是一樣的,均是表示ES的_id,如圖:
在這裏插入圖片描述
OK,16個對象,相當於重新查詢了ES16遍,每次查詢是22221條,22221x16=355536,數量基本吻合,沒有完全對上,是因爲上面代碼裏的scroll查詢,也出現過IceTimeOut異常,導致22221條還沒有完全查詢完即結束了。

最後這個問題,基本就定位到了,修復也就簡單了,把兩個Clear()方法,都移到catch後面的finally中,即可保證100%會調用,另外就是對接中間件的同事,針對IceTimeOut問題的解決了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章