您好,登錄后才能下訂單哦!
這篇文章主要講解了“如何定位內存泄露”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何定位內存泄露”吧!
上一節中我們嘗試了多種多線程方案,總會有各種各樣奇怪的問題。
于是最后決定使用生產-消費者模式去實現。
實現如下:
這里使用 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,最直觀的感受就是程序很慢。
這時候你就需要添加一下 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 個,這么多對象顯然不合理。
代碼給人的第一感受,就是和業務邏輯沒啥關系的寫文件了。
很多小伙伴肯定想到了可以使用 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("業務邏輯處理結果"); } }
全部改完之后,重新部署驗證,一切順利。
感謝各位的閱讀,以上就是“如何定位內存泄露”的內容了,經過本文的學習后,相信大家對如何定位內存泄露這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。