如何定位內存泄露

這篇文章主要講解了“如何定位內存泄露”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何定位內存泄露”吧!

超過10多年行業(yè)經(jīng)驗,技術領先,服務至上的經(jīng)營模式,全靠網(wǎng)絡和口碑獲得客戶,為自己降低成本,也就是為客戶降低成本。到目前業(yè)務范圍包括了:成都網(wǎng)站建設、成都網(wǎng)站設計,成都網(wǎng)站推廣,成都網(wǎng)站優(yōu)化,整體網(wǎng)絡托管,微信平臺小程序開發(fā),微信開發(fā),重慶APP開發(fā)公司,同時也可以讓客戶的網(wǎng)站和網(wǎng)絡營銷和我們一樣獲得訂單和生意!

生產(chǎn)-消費者模式

簡介

上一節(jié)中我們嘗試了多種多線程方案,總會有各種各樣奇怪的問題。

于是最后決定使用生產(chǎn)-消費者模式去實現(xiàn)。

實現(xiàn)如下:

這里使用 AtomicLong 做了一個簡單的計數(shù)。

userMapper.handle2(Arrays.asList(user)); 這個方法是同事以前的方法,當然做了很多簡化。

就沒有修改,入?yún)⑹且粋€列表。這里為了兼容,使用 Arrays.asList() 簡單封裝了一下。

import com.github.houbb.thread.demo.dal.entity.User; import com.github.houbb.thread.demo.dal.mapper.UserMapper; import com.github.houbb.thread.demo.service.UserService;  import java.util.Arrays; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong;  /**  * 分頁查詢  * @author binbin.hou  * @since 1.0.0  */ public class UserServicePageQueue implements UserService {      // 分頁大小     private final int pageSize = 10000;      private static final int THREAD_NUM = 20;      private final Executor executor = Executors.newFixedThreadPool(THREAD_NUM);      private final ArrayBlockingQueue<User> queue = new ArrayBlockingQueue<>(2 * pageSize, true);       // 模擬注入     private UserMapper userMapper = new UserMapper();      /**      * 計算總數(shù)      */     private AtomicLong counter = new AtomicLong(0);      // 消費線程任務     public class ConsumerTask implements Runnable {          @Override         public void run() {             while (true) {                 try {                     // 會阻塞直到獲取到元素                     User user = queue.take();                     userMapper.handle2(Arrays.asList(user));                      long count = counter.incrementAndGet();                 } catch (InterruptedException e) {                     e.printStackTrace();                 }             }         }     }      // 初始化消費者進程     // 啟動五個進程去處理     private void startConsumer() {         for(int i = 0; i < THREAD_NUM; i++) {             ConsumerTask task = new ConsumerTask();             executor.execute(task);         }     }      /**      * 處理所有的用戶      */     public void handleAllUser() {         // 啟動消費者         startConsumer();          // 充值計數(shù)器         counter = new AtomicLong(0);          // 分頁查詢         int total = userMapper.count();         int totalPage = total / pageSize;         for(int i = 1; i <= totalPage; i++) {             // 等待消費者處理已有的信息             awaitQueue(pageSize);              System.out.println(UserMapper.currentTime() + " 第 " + i + " 頁查詢開始");             List<User> userList = userMapper.selectList(i, pageSize);              // 直接往隊列里面扔             queue.addAll(userList);              System.out.println(UserMapper.currentTime() + " 第 " + i + " 頁查詢全部完成");         }     }      /**      * 等待,直到 queue 的小于等于 limit,才進行生產(chǎn)處理      *      * 首先判斷隊列的大小,可以調整為0的時候,才查詢。      * 不過因為查詢也比較耗時,所以可以調整為小于 pageSize 的時候就可以準備查詢      * 從而保障消費者不會等待太久      * @param limit 限制      */     private void awaitQueue(int limit) {         while (true) {             // 獲取阻塞隊列的大小             int size = queue.size();              if(size >= limit) {                 try {                     // 根據(jù)實際的情況進行調整                     Thread.sleep(1000);                 } catch (InterruptedException e) {                     e.printStackTrace();                 }             } else {                 break;             }         }     }  }

 測試驗證

當然這個方法在集成環(huán)境跑沒有任何的問題。

于是就開始直接上生產(chǎn)驗證,結果開始很快,然后就可以變慢了。

一看 GC 日志,梅開二度,F(xiàn)ULL GC。

可惡,圣斗士竟然會被同一招打敗 2 次嗎?

如何定位內存泄露

FULL GC 的產(chǎn)生

一般要發(fā)現(xiàn) full gc,最直觀的感受就是程序很慢。

這時候你就需要添加一下 GC 日志打印,看一下是否有 full gc 即可。

這個最坑的地方就在于,性能問題是測試一般無法驗證的,除非你進行壓測。

壓測還要同時滿足兩個條件:

(1)數(shù)據(jù)量足夠大,或者說 QPS 足夠高。持續(xù)壓

(2)資源足夠少,也就是還想馬兒跑,還想馬兒不吃草。

好巧不巧,我們同時趕上了兩點。

那么問題又來了,如何定位為什么 FULL GC 呢?

內存泄露

程序變慢并不是一開始就慢,而是開始很快,然后變慢,接著就是不停的 FULL GC。

這就和自然的想到是內存泄露。

如何定位內存泄露呢?

你可以分成下面幾步:

(1)看代碼,是否有明顯存在內存泄露的地方。然后修改驗證。如果無法解決,則找出可能存在問題的地方,執(zhí)行第二步。

(2)把 FULL GC 時的堆棧信息 dump 下來,分析到底是什么數(shù)據(jù)過大,然后結合 1 去解決。

接下來,讓我們一起看一下這個過程的簡化版本記錄。

問題定位

看代碼

最基本的生產(chǎn)者-消費者模式確認了即便,感覺沒啥問題。

于是就要看一下消費者模式中調用其他人的方法問題。

方法的核心目的

(1)遍歷入?yún)⒘斜?,?zhí)行業(yè)務處理。

(2)把當前批次的處理結果寫入到文件中。

方法實現(xiàn)

簡化版本如下:

/**  * 模擬用戶處理  *  * @param userList 用戶列表  */ public void handle2(List<User> userList) {     String targetDir = "D:\\data\\";     // 理論讓每一個線程只讀寫屬于自己的文件     String fileName = Thread.currentThread().getName()+".txt";     String fullFileName = targetDir + fileName;     FileWriter fileWriter = null;     BufferedWriter bufferedWriter = null;     User userExample;     try {         fileWriter = new FileWriter(fullFileName);         bufferedWriter = new BufferedWriter(fileWriter);         StringBuffer stringBuffer = null;         for(User user : userList) {             stringBuffer = new StringBuffer();              // 業(yè)務邏輯             userExample = new User();             userExample.setId(user.getId());             // 如果查詢到的結果已存在,則跳過處理             List<User> userCountList = queryUserList(userExample);             if(userCountList != null && userCountList.size() > 0) {                 return;             }             // 其他處理邏輯              // 記錄最后的結果             stringBuffer.append("用戶")                     .append(user.getId())                     .append("同步結果完成");             bufferedWriter.newLine();             bufferedWriter.write(stringBuffer.toString());         }         // 處理結果寫入到文件中         bufferedWriter.newLine();         bufferedWriter.flush();         bufferedWriter.close();         fileWriter.close();     } catch (Exception exception) {         exception.printStackTrace();     } finally {         try {             if (null != bufferedWriter) {                 bufferedWriter.close();             }             if (null != fileWriter) {                 fileWriter.close();             }         } catch (Exception e) {         }     } }

這種代碼怎么說呢,大概就是祖?zhèn)鞔a吧,不曉得大家有沒有見過,或者寫過呢?

我們可以不看文件部分,核心部分實際上只有:

User userExample; for(User user : userList) {     // 業(yè)務邏輯     userExample = new User();     userExample.setId(user.getId());     // 如果查詢到的結果已存在,則跳過處理     List<User> userCountList = queryUserList(userExample);     if(userCountList != null && userCountList.size() > 0) {         return;     }     // 其他處理邏輯 }

 代碼存在的問題

你覺得上面的代碼有哪些問題?

什么地方可能存在內存泄露呢?

有應該如何改進呢?

看堆棧

如果你看代碼已經(jīng)確定了疑惑的地方,那么接下來就是去看一下堆棧,驗證下自己的猜想。

堆棧的查看方式

jvm 堆棧查看的方式很多,我們這里以 jmap 命令為例。

(1)找到 java 進程的 pid

你可以執(zhí)行 jps 或者 ps ux 等,選擇一個你喜歡的。

我們 windows 本地測試了下(實際生產(chǎn)一般是 linux 系統(tǒng)):

D:\Program Files\Java\jdk1.8.0_192\bin>jps 11168 Jps 3440 RemoteMavenServer36 4512 11660 Launcher 11964 UserServicePageQueue

UserServicePageQueue 是我們執(zhí)行的測試程序,所以 pid 是 11964

(2)執(zhí)行 jmap 獲取堆棧信息

命令:

jmap -histo 11964

效果如下:

D:\Program Files\Java\jdk1.8.0_192\bin>jmap -histo 11964   num     #instances         #bytes  class name ----------------------------------------------    1:        161031       20851264  [C    2:        157949        3790776  java.lang.String    3:          1709        3699696  [B    4:          3472        3688440  [I    5:        139358        3344592  com.github.houbb.thread.demo.dal.entity.User    6:        139614        2233824  java.lang.Integer    7:         12716         508640  java.io.FileDescriptor    8:         12714         406848  java.io.FileOutputStream    9:          7122         284880  java.lang.ref.Finalizer   10:         12875         206000  java.lang.Object   ...

當然下面還有很多,你可以使用 head 命令過濾。

當然,如果服務器不支持這個命令,你可以把堆棧信息輸出到文件中:

jmap -histo 11964 >> dump.txt

堆棧分析

我們可以很明顯發(fā)現(xiàn)不合理的地方:

[C 這里指的是 chars,有 161031。

String 是字符串,有 157949。

當然還有 User 對象,有 139358。

我們每一次分頁是 1W 個,queue 中最多是 19999 個,這么多對象顯然不合理。

代碼中的問題

chars 和 String 為什么這么多

代碼給人的第一感受,就是和業(yè)務邏輯沒啥關系的寫文件了。

很多小伙伴肯定想到了可以使用 TWR 簡化一下代碼,不過這里存在兩個問題:

(1)最后文件中能記錄所有的執(zhí)行結果嗎?

(2)有沒有更好的方式呢?

對于問題1,答案是不能。雖然我們?yōu)槊恳粋€線程創(chuàng)建一個文件,但是實際測試,發(fā)現(xiàn)文件會被覆蓋。

實際上比起我們自己寫文件,更應該使用 log 去記錄結果,這樣更加優(yōu)雅。

于是,最后把代碼簡化如下:

//日志  User userExample; for(User user : userList) {     // 業(yè)務邏輯     userExample = new User();     userExample.setId(user.getId());     // 如果查詢到的結果已存在,則跳過處理     List<User> userCountList = queryUserList(userExample);     if(userCountList != null && userCountList.size() > 0) {         // 日志         return;     }     // 其他處理邏輯      // 日志記錄結果 }

user 對象為什么這里多?

我們看一下核心業(yè)務代碼:

User userExample; for(User user : userList) {     // 業(yè)務邏輯     userExample = new User();     userExample.setId(user.getId());     // 如果查詢到的結果已存在,則跳過處理     List<User> userCountList = queryUserList(userExample);     if(userCountList != null && userCountList.size() > 0) {         return;     }     // 其他處理邏輯 }

這里在判斷是否存在的時候構建了一個 mybatis 中常用的 User 查詢條件,然后判斷查詢的列表大小。

這里有兩個問題:

(1)判斷是否存在,最好使用 count,而不是判斷列表結果大小。

(2)User userExample 的作用域盡量小一點。

調整如下:

for(User user : userList) {     // 業(yè)務邏輯     User userExample = new User();     userExample.setId(user.getId());     // 如果查詢到的結果已存在,則跳過處理     int count = selectCount(userExample);     if(count > 0) {         return;     }     // 其他業(yè)務邏輯 }

 調整之后的代碼

這里的 System.out.println 實際使用時用 log 替代,這里只是為了演示。

/**  * 模擬用戶處理  *  * @param userList 用戶列表  */ public void handle3(List<User> userList) {     System.out.println("入?yún)ⅲ?quot; + userList);     for(User user : userList) {         // 業(yè)務邏輯         User userExample = new User();         userExample.setId(user.getId());         // 如果查詢到的結果已存在,則跳過處理         int count = selectCount(userExample);         if(count > 0) {             System.out.println("如果查詢到的結果已存在,則跳過處理");             continue;         }         // 其他業(yè)務邏輯         System.out.println("業(yè)務邏輯處理結果");     } }

 生產(chǎn)驗證

全部改完之后,重新部署驗證,一切順利。

感謝各位的閱讀,以上就是“如何定位內存泄露”的內容了,經(jīng)過本文的學習后,相信大家對如何定位內存泄露這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關知識點的文章,歡迎關注!

本文名稱:如何定位內存泄露
網(wǎng)站路徑:http://muchs.cn/article6/jcpeig.html

成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供移動網(wǎng)站建設全網(wǎng)營銷推廣、網(wǎng)站收錄、搜索引擎優(yōu)化網(wǎng)站維護、電子商務

廣告

聲明:本網(wǎng)站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

成都網(wǎng)站建設