中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何定位內存泄露

發布時間:2021-10-13 16:04:45 來源:億速云 閱讀:139 作者:iii 欄目:編程語言

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

生產-消費者模式

簡介

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

于是最后決定使用生產-消費者模式去實現。

實現如下:

這里使用 AtomicLong 做了一個簡單的計數。

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

就沒有修改,入參是一個列表。這里為了兼容,使用 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();      /**      * 計算總數      */     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();          // 充值計數器         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,才進行生產處理      *      * 首先判斷隊列的大小,可以調整為0的時候,才查詢。      * 不過因為查詢也比較耗時,所以可以調整為小于 pageSize 的時候就可以準備查詢      * 從而保障消費者不會等待太久      * @param limit 限制      */     private void awaitQueue(int limit) {         while (true) {             // 獲取阻塞隊列的大小             int size = queue.size();              if(size >= limit) {                 try {                     // 根據實際的情況進行調整                     Thread.sleep(1000);                 } catch (InterruptedException e) {                     e.printStackTrace();                 }             } else {                 break;             }         }     }  }

 測試驗證

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

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

一看 GC 日志,梅開二度,FULL GC。

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

如何定位內存泄露

FULL GC 的產生

一般要發現 full gc,最直觀的感受就是程序很慢。

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

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

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

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

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

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

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

內存泄露

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

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

如何定位內存泄露呢?

你可以分成下面幾步:

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

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

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

問題定位

看代碼

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

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

方法的核心目的

(1)遍歷入參列表,執行業務處理。

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

方法實現

簡化版本如下:

/**  * 模擬用戶處理  *  * @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();              // 業務邏輯             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) {         }     } }

這種代碼怎么說呢,大概就是祖傳代碼吧,不曉得大家有沒有見過,或者寫過呢?

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

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

 代碼存在的問題

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

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

有應該如何改進呢?

看堆棧

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

堆棧的查看方式

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

(1)找到 java 進程的 pid

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

我們 windows 本地測試了下(實際生產一般是 linux 系統):

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

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

(2)執行 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

堆棧分析

我們可以很明顯發現不合理的地方:

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

String 是字符串,有 157949。

當然還有 User 對象,有 139358。

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

代碼中的問題

chars 和 String 為什么這么多

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

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

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

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

對于問題1,答案是不能。雖然我們為每一個線程創建一個文件,但是實際測試,發現文件會被覆蓋。

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

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

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

user 對象為什么這里多?

我們看一下核心業務代碼:

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

 調整之后的代碼

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

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

 生產驗證

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

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

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

jvm
AI

龙南县| 将乐县| 桐乡市| 罗平县| 夏津县| 临夏市| 交城县| 健康| 六枝特区| 德阳市| 利辛县| 宁强县| 建瓯市| 雷山县| 五原县| 慈利县| 阿克苏市| 咸宁市| 新竹县| 屏南县| 崇仁县| 乐山市| 台江县| 花莲市| 富川| 桐梓县| 凤庆县| 英吉沙县| 静乐县| 泰来县| 海兴县| 车致| 岳池县| 郎溪县| 藁城市| 曲水县| 舞钢市| 赞皇县| 大姚县| 高安市| 榆中县|