成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

JVM FULL GC 生產問題筆記

開發 后端
造成 full gc 的原因一般都是內存泄漏。GC 日志真的很重要,遇到問題一定要記得添加上,這樣才能更好的分析解決問題。

故事的開始

早晨 8 點多,同事給我發了一條消息。

“跑批程序很慢,負載過高,上午幫忙看一下。”

我一邊走路,一遍回復好的,整個人都是懵的,一方面是因為沒睡飽,另一方面是因為對同事的程序一無所知。

而這,就是今天整個故事的開始。

[[392421]]

問題的定位

到了公司,簡單了解情況之后,開始登陸機器,查看日志。

一看好家伙,最簡單的一個請求 10S+,換做實時鏈路估計直接炸鍋了。

于是想到兩種可能:

(1)數據庫有慢 SQL,歸檔等嚴重影響性能的操作

(2)應用 FULL GC

于是讓 DBA 幫忙定位是否有第一種情況的問題,自己登陸機器看是否有 FULL GC。

初步的解決

十幾分鐘后,DBA 告訴我確實有慢 SQL,已經 kill 掉了。

GC 日志

不過查看 GC 日志的道路卻一點都不順利。

(1)發現應用本身沒打印 gc log

(2)想使用 jstat 發現 docker 用戶沒權限,醉了。

于是讓配管幫忙重新配置 jvm 參數加上 gc 日志,幸運的是,這個程序屬于跑批程序,可以隨時發布。

剩下的就等同事來了,下午驗證一下即可。

FULL-GC 的源頭

慢的源頭

有了 GC 日志之后,很快就定位到慢是因為一直在發生 full gc 導致的。

那么為什么會一直有 full gc 呢?

jvm 配置的調整

一開始大家都以為是 jvm 的新生代配置的太小了,于是重新調整了 jvm 的參數配置。

結果很不幸,執行不久之后還是會觸發 full gc。

要定位 full gc 的源頭,只有開始看代碼了。

[[392422]]

代碼與需求

需求

首先說一下應用內需要解決的問題還是比較簡單的。

把數據庫里的數據全部查出來,依次執行處理,不過有兩點需要注意:

(1)數據量相對較大,百萬級

(2)單條數據處理比較慢,希望處理的盡可能快。

業務簡化

為了便于大家理解,我們這里簡化所有的業務,使用最簡單的 User 類來模擬業務。

  • User.java

基本的數據庫實體。

  1. /** 
  2.  * 用戶信息 
  3.  * @author binbin.hou 
  4.  * @since 1.0.0 
  5.  */ 
  6. public class User { 
  7.  
  8.     private Integer id; 
  9.  
  10.     public Integer getId() { 
  11.         return id; 
  12.     } 
  13.  
  14.     public void setId(Integer id) { 
  15.         this.id = id; 
  16.     } 
  17.  
  18.     @Override 
  19.     public String toString() { 
  20.         return "User{" + 
  21.                 "id=" + id + 
  22.                 '}'
  23.     } 
  24.  
  •  UserMapper.java

模擬數據庫查詢操作。

  1. public class UserMapper { 
  2.  
  3.     // 總數,可以根據實際調整為 100W+ 
  4.     private static final int TOTAL = 100; 
  5.  
  6.     public int count() { 
  7.         return TOTAL; 
  8.     } 
  9.  
  10.     public List<User> selectAll() { 
  11.         return selectList(1, TOTAL); 
  12.     } 
  13.  
  14.     public List<User> selectList(int pageNum, int pageSize) { 
  15.         List<User> list = new ArrayList<User>(pageSize); 
  16.  
  17.         int start = (pageNum - 1) * pageSize; 
  18.         for (int i = start; i < start + pageSize; i++) { 
  19.             User user = new User(); 
  20.             user.setId(i); 
  21.             list.add(user); 
  22.         } 
  23.  
  24.         return list; 
  25.     } 
  26.  
  27.     /** 
  28.      * 模擬用戶處理 
  29.      * 
  30.      * @param user 用戶 
  31.      */ 
  32.     public void handle(User user) { 
  33.         try { 
  34.             // 模擬不同的耗時 
  35.             int id = user.getId(); 
  36.             if(id % 2 == 0) { 
  37.                 Thread.sleep(100); 
  38.             } else { 
  39.                 Thread.sleep(200); 
  40.             } 
  41.         } catch (InterruptedException e) { 
  42.             e.printStackTrace(); 
  43.         } 
  44.         System.out.println(System.currentTimeMillis() + " " + Thread.currentThread().getName() + " " + user); 
  45.     } 
  46.  

 這里提供了幾個簡單的方法,這里為了演示方便,將總數固定為 100。

  • UserService.java

定義需要處理所有實體的一個接口。

  1. /** 
  2.  * 用戶服務接口 
  3.  * @author binbin.hou 
  4.  * @since 1.0.0 
  5.  */ 
  6. public interface UserService { 
  7.  
  8.  
  9.     /** 
  10.      * 處理所有的用戶 
  11.      */ 
  12.     void handleAllUser(); 
  13.  

 v1-全部加載到內存

最簡單粗暴的方式,就是把所有數據直接加載到內存。

  1. public class UserServiceAll implements UserService { 
  2.  
  3.  
  4.     /** 
  5.      * 處理所有的用戶 
  6.      */ 
  7.     public void handleAllUser() { 
  8.         UserMapper userMapper = new UserMapper(); 
  9.         // 全部加載到內存 
  10.  
  11.         List<User> userList = userMapper.selectAll(); 
  12.         for(User user : userList) { 
  13.             // 處理單個用戶 
  14.             userMapper.handle(user); 
  15.         } 
  16.     } 
  17.  

 這種方式非常的簡單,容易理解。

不過缺點也比較大,數據量較大的時候會直接把內存打爆。

我也嘗試了一下這種方式,應用直接假死,所以不可行。

v2-分頁加載到內存

既然不能一把加載,那我很自然的就想到分頁。

  1. /** 
  2.  * 分頁查詢 
  3.  * @author binbin.hou 
  4.  * @since 1.0.0 
  5.  */ 
  6. public class UserServicePage implements UserService { 
  7.  
  8.     /** 
  9.      * 處理所有的用戶 
  10.      */ 
  11.     public void handleAllUser() { 
  12.         UserMapper userMapper = new UserMapper(); 
  13.         // 分頁查詢 
  14.         int total = userMapper.count(); 
  15.         int pageSize = 10; 
  16.  
  17.         int totalPage = total / pageSize; 
  18.         for(int i = 1; i <= totalPage; i++) { 
  19.             System.out.println("第" + i + " 頁查詢開始"); 
  20.             List<User> userList = userMapper.selectList(i, pageSize); 
  21.  
  22.             for(User user : userList) { 
  23.                 // 處理單個用戶 
  24.                 userMapper.handle(user); 
  25.             } 
  26.         } 
  27.     } 
  28.  

 一般這樣處理也就夠了,不過因為想追求更快的處理速度,同事使用了多線程,大概實現如下。

v3-分頁多線程

這里使用 Executor 線程池進行單個數據的消費處理。

主要注意點有兩個地方:

(1)使用 sublist 控制每一個線程處理的數據范圍

(2)使用 CountDownLatch 保證當前頁處理完成后,才進行到下一次分頁的查詢和處理。

  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.List; 
  6. import java.util.concurrent.CountDownLatch; 
  7. import java.util.concurrent.Executor; 
  8. import java.util.concurrent.Executors; 
  9.  
  10. /** 
  11.  * 分頁查詢多線程 
  12.  * @author binbin.hou 
  13.  * @since 1.0.0 
  14.  */ 
  15. public class UserServicePageExecutor implements UserService { 
  16.  
  17.     private static final int THREAD_NUM = 5; 
  18.  
  19.     private static final Executor EXECUTOR = Executors.newFixedThreadPool(THREAD_NUM); 
  20.  
  21.     /** 
  22.      * 處理所有的用戶 
  23.      */ 
  24.     public void handleAllUser() { 
  25.         UserMapper userMapper = new UserMapper(); 
  26.         // 分頁查詢 
  27.         int total = userMapper.count(); 
  28.         int pageSize = 10; 
  29.  
  30.         int totalPage = total / pageSize; 
  31.         for(int i = 1; i <= totalPage; i++) { 
  32.             System.out.println("第 " + i + " 頁查詢開始"); 
  33.             List<User> userList = userMapper.selectList(i, pageSize); 
  34.  
  35.             // 使用多線程處理 
  36.             int count = userList.size(); 
  37.             int countPerThread = count / THREAD_NUM; 
  38.  
  39.             // 通過 CountDownLatch 保證當前分頁執行完成,才繼續下一個分頁的處理。 
  40.             CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM); 
  41.             for(int j = 0; j < THREAD_NUM; j++) { 
  42.                 int startIndex = j * countPerThread; 
  43.                 int endIndex = startIndex + countPerThread; 
  44.                 // 最后一個 
  45.                 if(j == THREAD_NUM - 1) { 
  46.                     endIndex = count
  47.                 } 
  48.  
  49.                 final int finalStartIndex = startIndex; 
  50.                 final int finalEndIndex = endIndex; 
  51.                 EXECUTOR.execute(()->{ 
  52.                     List<User> subList = userList.subList(finalStartIndex, finalEndIndex); 
  53.                     handleList(subList); 
  54.  
  55.                     // countdown 
  56.                     countDownLatch.countDown(); 
  57.                 }); 
  58.             } 
  59.  
  60.  
  61.             try { 
  62.                 countDownLatch.await(); 
  63.  
  64.                 System.out.println("第 " + i + " 頁查詢全部完成"); 
  65.             } catch (InterruptedException e) { 
  66.                 e.printStackTrace(); 
  67.             } 
  68.         } 
  69.     } 
  70.  
  71.     private void handleList(List<User> userList) { 
  72.         UserMapper userMapper = new UserMapper(); 
  73.  
  74.         // 處理 
  75.         for(User user : userList) { 
  76.             // 處理單個用戶 
  77.             userMapper.handle(user); 
  78.         } 
  79.     } 
  80.  

 這個實現是有一點復雜,但是第一感覺還是沒啥問題。

為什么就 full gc 了呢?

sublist 的坑

這里使用了 sublist 方法,性能很好,也達到了分割范圍的作用。

不過一開始,我卻懷疑這里導致了內存泄漏。

SubList 的源碼:

  1. private class SubList extends AbstractList<E> implements RandomAccess { 
  2.         private final AbstractList<E> parent; 
  3.         private final int parentOffset; 
  4.         private final int offset; 
  5.         int size
  6.  
  7.         SubList(AbstractList<E> parent, 
  8.                 int offset, int fromIndex, int toIndex) { 
  9.             this.parent = parent; 
  10.             this.parentOffset = fromIndex; 
  11.             this.offset = offset + fromIndex; 
  12.             this.size = toIndex - fromIndex; 
  13.             this.modCount = ArrayList.this.modCount; 
  14.         } 

 可以看出SubList原理:

  1. 保存父ArrayList的引用;
  2. 通過計算offset和size表示subList在原始list的范圍;

由此可知,這種方式的subList保存對原始list的引用,而且是強引用,導致GC不能回收,故而導致內存泄漏,當程序運行一段時間后,程序無法再申請內存,拋出內存溢出錯誤。

解決思路是使用工具類替代掉 sublist 方法,缺點是內存占用會變多,比如:

  1. /** 
  2.  * @author binbin.hou 
  3.  * @since 1.0.0 
  4.  */ 
  5. public class ListUtils { 
  6.  
  7.     @SuppressWarnings("all"
  8.     public static List copyList(List list, int start, int end) { 
  9.         List results = new ArrayList(); 
  10.         for(int i = start; i < end; i++) { 
  11.             results.add(list.get(i)); 
  12.         } 
  13.         return results; 
  14.     } 
  15.  

 經過實測,發現并不是這個原因導致的。orz

lambda 的坑

因為使用的 jdk8,所以大家也就習慣性的使用 lambda 表達式。

  1. EXECUTOR.execute(()->{ 
  2.     //... 
  3. }); 

 這里實際上是一個語法糖,會導致 executor 引用 sublist。

因為 executor 的生命周期是非常長的,從而會讓 sublist 一直得不到釋放。

后來把代碼調整了如下,full gc 也確認解決了。

v4-分頁多線程 Task

我們使用 Task,讓 sublist 放在 task 中去處理。

  1. public class UserServicePageExecutorTask implements UserService { 
  2.  
  3.     private static final int THREAD_NUM = 5; 
  4.  
  5.     private static final Executor EXECUTOR = Executors.newFixedThreadPool(THREAD_NUM); 
  6.  
  7.     /** 
  8.      * 處理所有的用戶 
  9.      */ 
  10.     public void handleAllUser() { 
  11.         UserMapper userMapper = new UserMapper(); 
  12.         // 分頁查詢 
  13.         int total = userMapper.count(); 
  14.         int pageSize = 10; 
  15.  
  16.         int totalPage = total / pageSize; 
  17.         for(int i = 1; i <= totalPage; i++) { 
  18.             System.out.println("第 " + i + " 頁查詢開始"); 
  19.             List<User> userList = userMapper.selectList(i, pageSize); 
  20.  
  21.             // 使用多線程處理 
  22.             int count = userList.size(); 
  23.             int countPerThread = count / THREAD_NUM; 
  24.  
  25.             // 通過 CountDownLatch 保證當前分頁執行完成,才繼續下一個分頁的處理。 
  26.             CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM); 
  27.             for(int j = 0; j < THREAD_NUM; j++) { 
  28.                 int startIndex = j * countPerThread; 
  29.                 int endIndex = startIndex + countPerThread; 
  30.                 // 最后一個 
  31.                 if(j == THREAD_NUM - 1) { 
  32.                     endIndex = count
  33.                 } 
  34.  
  35.                 Task task = new Task(countDownLatch, userList, startIndex, endIndex); 
  36.                 EXECUTOR.execute(task); 
  37.             } 
  38.  
  39.             try { 
  40.                 countDownLatch.await(); 
  41.  
  42.                 System.out.println("第 " + i + " 頁查詢全部完成"); 
  43.             } catch (InterruptedException e) { 
  44.                 e.printStackTrace(); 
  45.             } 
  46.         } 
  47.     } 
  48.  
  49.     private void handleList(List<User> userList) { 
  50.         UserMapper userMapper = new UserMapper(); 
  51.  
  52.         // 處理 
  53.         for(User user : userList) { 
  54.             // 處理單個用戶 
  55.             userMapper.handle(user); 
  56.         } 
  57.     } 
  58.  
  59.     private class Task implements Runnable { 
  60.  
  61.         private final CountDownLatch countDownLatch; 
  62.  
  63.         private final List<User> allList; 
  64.  
  65.         private final int startIndex; 
  66.  
  67.         private final int endIndex; 
  68.  
  69.         private Task(CountDownLatch countDownLatch, List<User> allList, int startIndex, int endIndex) { 
  70.             this.countDownLatch = countDownLatch; 
  71.             this.allList = allList; 
  72.             this.startIndex = startIndex; 
  73.             this.endIndex = endIndex; 
  74.         } 
  75.  
  76.         @Override 
  77.         public void run() { 
  78.             try { 
  79.                 List<User> subList = allList.subList(startIndex, endIndex); 
  80.                 handleList(subList); 
  81.             } catch (Exception exception) { 
  82.                 exception.printStackTrace(); 
  83.             } finally { 
  84.                 countDownLatch.countDown(); 
  85.             } 
  86.         } 
  87.     } 
  88.  

 我們這里做了一點上面沒有考慮到的點,countDownLatch 可能無法被執行,導致線程被卡主。

于是我們把 countDownLatch.countDown(); 放在 finally 中去執行。

辛苦搞了大半天,按理說到這里故事應該就結束了,不過現實比理論更加夢幻。

實際執行的時候,這個程序總是會卡主一段時間,導致整體的效果很差,還沒有不適用多線程的效果好。

和其他同事溝通了一下,還是建議使用 生產-消費者 模式去實現比較好,原因如下:

(1)實現相對簡單,不會產生奇奇怪怪的 BUG

(2)相對于 countDownLatch 的強制等待,生產-消費者模式可以做到基本無鎖,性能更好。

于是,我晚上就花時間寫了一個簡單的 demo。

 v5-生產消費者模式

這里我們使用 ArrayBlockingQueue 作為阻塞隊列,也就是消息的存儲媒介。

當然,你也可以使用公司的 mq 中間件來實現類似的效果。

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

 整體的實現確實簡單很多,因為查詢比處理一般要快,所以往隊列中添加元素時,這里進行了等待。

當然可以根據你的實際業務進行調整等待時間等。

這里保證小于等于 pageSize 時才插入新的元素,保證不超過隊列的總長度,同時盡可能的讓消費者不會進入空閑等待狀態。

小結

總的來說,造成 full gc 的原因一般都是內存泄漏。

GC 日志真的很重要,遇到問題一定要記得添加上,這樣才能更好的分析解決問題。

很多技術知識,我們以為熟悉了,往往還是存在不少坑。

要永遠記得如無必要,勿增實體。

 

責任編輯:姜華 來源: 今日頭條
相關推薦

2021-04-14 10:14:34

JVM生產問題定位內存泄露

2025-04-24 09:01:37

2022-12-17 19:49:37

GCJVM故障

2020-07-29 15:01:50

JVMGCJDK

2020-03-03 17:35:09

Full GCMinor

2025-03-31 04:25:00

2017-09-26 16:32:03

JavaGC分析

2019-12-10 08:59:55

JVM內存算法

2009-07-08 15:11:58

JVM GC調整優化

2022-05-27 08:01:36

JVM內存收集器

2023-12-07 12:21:04

GCJVM垃圾

2017-11-08 15:23:57

Java GC優化jvm

2012-01-11 11:07:04

JavaJVM

2023-08-28 07:02:10

2010-09-26 16:55:31

JVM學習筆記

2017-06-09 08:49:07

加載器Full GCJVM

2019-09-02 14:53:53

JVM內存布局GC

2021-01-21 08:00:25

JVM

2017-09-21 14:40:06

jvm算法收集器

2009-07-09 16:23:36

java jvm
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 天天碰夜夜操 | 精品久久精品 | 国产精品国产精品国产专区不蜜 | 成人av一区| 日中文字幕在线 | 91免费小视频 | 日韩视频在线观看 | 成人av在线播放 | 日韩免费在线 | 九九99靖品 | 91久久国产综合久久 | 中文字幕在线免费视频 | 91福利网 | 国产精品久久久久久婷婷天堂 | 日韩免费视频一区二区 | 美女久久久久久久久 | 亚洲二区精品 | 天堂av影院| 天堂成人国产精品一区 | 久久夜视频| 成人高清视频在线观看 | 欧美国产日韩一区 | 精品99爱视频在线观看 | 日韩一区二区福利 | 国产精品久久久久一区二区三区 | 三级黄色片在线 | 中文字幕av在线 | 亚洲+变态+欧美+另类+精品 | 国产欧美一区二区在线观看 | 亚洲精品片 | 热久久久| 久久综合一区二区 | 天天看天天操 | 天天操欧美 | 亚洲国产成人精品女人 | 国产成人久久精品一区二区三区 | 精品96久久久久久中文字幕无 | 久久午夜视频 | 久久精品在线免费视频 | 伊人色综合久久久天天蜜桃 | 国产69精品久久99不卡免费版 |