自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JVM FULL GC 生產(chǎn)問題 II-如何定位內(nèi)存泄露?

開發(fā) 后端
生產(chǎn) full gc 是一個比較麻煩的問題,一個是難以復(fù)現(xiàn),另一個是如果是偶發(fā)性的,又是實時鏈路,可能也不好執(zhí)行 dump 命令。所以寫代碼還是寫的盡可能簡單的好,不然會有各種問題。

[[393045]]

情景回顧

我們在上一篇 JVM FULL GC 生產(chǎn)問題筆記 中提出了如何更好的實現(xiàn)一個多線程消費的實現(xiàn)方式。

沒有看過的小伙伴建議看一下。

本來以為一切都可以結(jié)束的,不過又發(fā)生了一點點意外,這里記錄一下,避免自己和小伙伴們踩坑。

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

簡介

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

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

實現(xiàn)如下:

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

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

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

  1. import com.github.houbb.thread.demo.dal.entity.User
  2. import com.github.houbb.thread.demo.dal.mapper.UserMapper; 
  3. import com.github.houbb.thread.demo.service.UserService; 
  4.  
  5. import java.util.Arrays; 
  6. import java.util.List; 
  7. import java.util.concurrent.*; 
  8. import java.util.concurrent.atomic.AtomicLong; 
  9.  
  10. /** 
  11.  * 分頁查詢 
  12.  * @author binbin.hou 
  13.  * @since 1.0.0 
  14.  */ 
  15. public class UserServicePageQueue implements UserService { 
  16.  
  17.     // 分頁大小 
  18.     private final int pageSize = 10000; 
  19.  
  20.     private static final int THREAD_NUM = 20; 
  21.  
  22.     private final Executor executor = Executors.newFixedThreadPool(THREAD_NUM); 
  23.  
  24.     private final ArrayBlockingQueue<User> queue = new ArrayBlockingQueue<>(2 * pageSize, true); 
  25.  
  26.  
  27.     // 模擬注入 
  28.     private UserMapper userMapper = new UserMapper(); 
  29.  
  30.     /** 
  31.      * 計算總數(shù) 
  32.      */ 
  33.     private AtomicLong counter = new AtomicLong(0); 
  34.  
  35.     // 消費線程任務(wù) 
  36.     public class ConsumerTask implements Runnable { 
  37.  
  38.         @Override 
  39.         public void run() { 
  40.             while (true) { 
  41.                 try { 
  42.                     // 會阻塞直到獲取到元素 
  43.                     User user = queue.take(); 
  44.                     userMapper.handle2(Arrays.asList(user)); 
  45.  
  46.                     long count = counter.incrementAndGet(); 
  47.                 } catch (InterruptedException e) { 
  48.                     e.printStackTrace(); 
  49.                 } 
  50.             } 
  51.         } 
  52.     } 
  53.  
  54.     // 初始化消費者進程 
  55.     // 啟動五個進程去處理 
  56.     private void startConsumer() { 
  57.         for(int i = 0; i < THREAD_NUM; i++) { 
  58.             ConsumerTask task = new ConsumerTask(); 
  59.             executor.execute(task); 
  60.         } 
  61.     } 
  62.  
  63.     /** 
  64.      * 處理所有的用戶 
  65.      */ 
  66.     public void handleAllUser() { 
  67.         // 啟動消費者 
  68.         startConsumer(); 
  69.  
  70.         // 充值計數(shù)器 
  71.         counter = new AtomicLong(0); 
  72.  
  73.         // 分頁查詢 
  74.         int total = userMapper.count(); 
  75.         int totalPage = total / pageSize; 
  76.         for(int i = 1; i <= totalPage; i++) { 
  77.             // 等待消費者處理已有的信息 
  78.             awaitQueue(pageSize); 
  79.  
  80.             System.out.println(UserMapper.currentTime() + " 第 " + i + " 頁查詢開始"); 
  81.             List<User> userList = userMapper.selectList(i, pageSize); 
  82.  
  83.             // 直接往隊列里面扔 
  84.             queue.addAll(userList); 
  85.  
  86.             System.out.println(UserMapper.currentTime() + " 第 " + i + " 頁查詢?nèi)客瓿?quot;); 
  87.         } 
  88.     } 
  89.  
  90.     /** 
  91.      * 等待,直到 queue 的小于等于 limit,才進行生產(chǎn)處理 
  92.      * 
  93.      * 首先判斷隊列的大小,可以調(diào)整為0的時候,才查詢。 
  94.      * 不過因為查詢也比較耗時,所以可以調(diào)整為小于 pageSize 的時候就可以準備查詢 
  95.      * 從而保障消費者不會等待太久 
  96.      * @param limit 限制 
  97.      */ 
  98.     private void awaitQueue(int limit) { 
  99.         while (true) { 
  100.             // 獲取阻塞隊列的大小 
  101.             int size = queue.size(); 
  102.  
  103.             if(size >= limit) { 
  104.                 try { 
  105.                     // 根據(jù)實際的情況進行調(diào)整 
  106.                     Thread.sleep(1000); 
  107.                 } catch (InterruptedException e) { 
  108.                     e.printStackTrace(); 
  109.                 } 
  110.             } else { 
  111.                 break; 
  112.             } 
  113.         } 
  114.     } 
  115.  

 測試驗證

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

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

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

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

[[393046]]

FULL GC 的產(chǎn)生

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

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

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

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

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

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

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

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

內(nèi)存泄露

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

這就和自然的想到是內(nèi)存泄露。

如何定位內(nèi)存泄露呢?

你可以分成下面幾步:

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

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

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

問題定位

看代碼

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

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

方法的核心目的

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

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

方法實現(xiàn)

簡化版本如下:

  1. /** 
  2.  * 模擬用戶處理 
  3.  * 
  4.  * @param userList 用戶列表 
  5.  */ 
  6. public void handle2(List<User> userList) { 
  7.     String targetDir = "D:\\data\\"
  8.     // 理論讓每一個線程只讀寫屬于自己的文件 
  9.     String fileName = Thread.currentThread().getName()+".txt"
  10.     String fullFileName = targetDir + fileName; 
  11.     FileWriter fileWriter = null
  12.     BufferedWriter bufferedWriter = null
  13.     User userExample; 
  14.     try { 
  15.         fileWriter = new FileWriter(fullFileName); 
  16.         bufferedWriter = new BufferedWriter(fileWriter); 
  17.         StringBuffer stringBuffer = null
  18.         for(User user : userList) { 
  19.             stringBuffer = new StringBuffer(); 
  20.  
  21.             // 業(yè)務(wù)邏輯 
  22.             userExample = new User(); 
  23.             userExample.setId(user.getId()); 
  24.             // 如果查詢到的結(jié)果已存在,則跳過處理 
  25.             List<User> userCountList = queryUserList(userExample); 
  26.             if(userCountList != null && userCountList.size() > 0) { 
  27.                 return
  28.             } 
  29.             // 其他處理邏輯 
  30.  
  31.             // 記錄最后的結(jié)果 
  32.             stringBuffer.append("用戶"
  33.                     .append(user.getId()) 
  34.                     .append("同步結(jié)果完成"); 
  35.             bufferedWriter.newLine(); 
  36.             bufferedWriter.write(stringBuffer.toString()); 
  37.         } 
  38.         // 處理結(jié)果寫入到文件中 
  39.         bufferedWriter.newLine(); 
  40.         bufferedWriter.flush(); 
  41.         bufferedWriter.close(); 
  42.         fileWriter.close(); 
  43.     } catch (Exception exception) { 
  44.         exception.printStackTrace(); 
  45.     } finally { 
  46.         try { 
  47.             if (null != bufferedWriter) { 
  48.                 bufferedWriter.close(); 
  49.             } 
  50.             if (null != fileWriter) { 
  51.                 fileWriter.close(); 
  52.             } 
  53.         } catch (Exception e) { 
  54.         } 
  55.     } 

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

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

  1. User userExample; 
  2. for(User user : userList) { 
  3.     // 業(yè)務(wù)邏輯 
  4.     userExample = new User(); 
  5.     userExample.setId(user.getId()); 
  6.     // 如果查詢到的結(jié)果已存在,則跳過處理 
  7.     List<User> userCountList = queryUserList(userExample); 
  8.     if(userCountList != null && userCountList.size() > 0) { 
  9.         return
  10.     } 
  11.     // 其他處理邏輯 

 代碼存在的問題

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

什么地方可能存在內(nèi)存泄露呢?

有應(yīng)該如何改進呢?

看堆棧

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

堆棧的查看方式

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

(1)找到 java 進程的 pid

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

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

  1. D:\Program Files\Java\jdk1.8.0_192\bin>jps 
  2. 11168 Jps 
  3. 3440 RemoteMavenServer36 
  4. 4512 
  5. 11660 Launcher 
  6. 11964 UserServicePageQueue 

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

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

命令:

  1. jmap -histo 11964 

效果如下:

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

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

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

  1. jmap -histo 11964 >> dump.txt 

堆棧分析

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

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

String 是字符串,有 157949。

當(dāng)然還有 User 對象,有 139358。

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

代碼中的問題

chars 和 String 為什么這么多

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

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

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

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

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

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

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

  1. //日志 
  2.  
  3. User userExample; 
  4. for(User user : userList) { 
  5.     // 業(yè)務(wù)邏輯 
  6.     userExample = new User(); 
  7.     userExample.setId(user.getId()); 
  8.     // 如果查詢到的結(jié)果已存在,則跳過處理 
  9.     List<User> userCountList = queryUserList(userExample); 
  10.     if(userCountList != null && userCountList.size() > 0) { 
  11.         // 日志 
  12.         return
  13.     } 
  14.     // 其他處理邏輯 
  15.  
  16.     // 日志記錄結(jié)果 

 user 對象為什么這里多?

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

  1. User userExample; 
  2. for(User user : userList) { 
  3.     // 業(yè)務(wù)邏輯 
  4.     userExample = new User(); 
  5.     userExample.setId(user.getId()); 
  6.     // 如果查詢到的結(jié)果已存在,則跳過處理 
  7.     List<User> userCountList = queryUserList(userExample); 
  8.     if(userCountList != null && userCountList.size() > 0) { 
  9.         return
  10.     } 
  11.     // 其他處理邏輯 

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

這里有兩個問題:

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

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

調(diào)整如下:

  1. for(User user : userList) { 
  2.     // 業(yè)務(wù)邏輯 
  3.     User userExample = new User(); 
  4.     userExample.setId(user.getId()); 
  5.     // 如果查詢到的結(jié)果已存在,則跳過處理 
  6.     int count = selectCount(userExample); 
  7.     if(count > 0) { 
  8.         return
  9.     } 
  10.     // 其他業(yè)務(wù)邏輯 

 調(diào)整之后的代碼

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

  1. /** 
  2.  * 模擬用戶處理 
  3.  * 
  4.  * @param userList 用戶列表 
  5.  */ 
  6. public void handle3(List<User> userList) { 
  7.     System.out.println("入?yún)ⅲ?quot; + userList); 
  8.     for(User user : userList) { 
  9.         // 業(yè)務(wù)邏輯 
  10.         User userExample = new User(); 
  11.         userExample.setId(user.getId()); 
  12.         // 如果查詢到的結(jié)果已存在,則跳過處理 
  13.         int count = selectCount(userExample); 
  14.         if(count > 0) { 
  15.             System.out.println("如果查詢到的結(jié)果已存在,則跳過處理"); 
  16.             continue
  17.         } 
  18.         // 其他業(yè)務(wù)邏輯 
  19.         System.out.println("業(yè)務(wù)邏輯處理結(jié)果"); 
  20.     } 

 生產(chǎn)驗證

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

希望不會有第三篇。:)

小結(jié)

當(dāng)然驗證的過程中還發(fā)生過一點小插曲,比如開發(fā)沒有權(quán)限看堆棧信息,執(zhí)行命令時程序已經(jīng)假死等等。

生產(chǎn) full gc 是一個比較麻煩的問題,一個是難以復(fù)現(xiàn),另一個是如果是偶發(fā)性的,又是實時鏈路,可能也不好執(zhí)行 dump 命令。

所以寫代碼還是寫的盡可能簡單的好,不然會有各種問題。

能復(fù)用已有的工具、中間件盡量復(fù)用。

這樣看來,我們自己寫的生產(chǎn)-消費者模式也不太好,因為復(fù)用性不強,所以建議使用公司已有的 mq 工具,不過如何選擇,還是看具體的業(yè)務(wù)場景。

架構(gòu),就是權(quán)衡。

希望本文對你有所幫助!

 

責(zé)任編輯:姜華 來源: 今日頭條
相關(guān)推薦

2021-04-12 09:36:14

JVM生產(chǎn)問題JVM FULL GC

2022-06-27 11:20:13

工具內(nèi)存GO

2022-02-07 08:55:57

Go程序代碼

2013-04-09 14:49:18

Linux內(nèi)存統(tǒng)計內(nèi)存泄露

2022-05-27 08:01:36

JVM內(nèi)存收集器

2019-12-10 08:59:55

JVM內(nèi)存算法

2020-02-27 13:01:57

JVM內(nèi)存劃分

2025-04-24 09:01:37

2012-01-11 11:07:04

JavaJVM

2022-12-17 19:49:37

GCJVM故障

2025-03-31 04:25:00

2020-06-23 09:48:09

Python開發(fā)內(nèi)存

2019-09-02 14:53:53

JVM內(nèi)存布局GC

2020-07-29 15:01:50

JVMGCJDK

2017-11-15 19:30:08

Python內(nèi)存泄露循環(huán)引用

2019-11-05 08:24:34

JavaOOM快速定位

2012-03-02 14:20:46

JavaJVM

2013-12-23 09:25:21

2020-03-03 17:35:09

Full GCMinor

2017-12-11 11:00:27

內(nèi)存泄露判斷
點贊
收藏

51CTO技術(shù)棧公眾號