av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

高并發(fā)服務優(yōu)化篇:詳解一次由讀寫鎖引起的內(nèi)存泄漏

高并發(fā)服務優(yōu)化篇:詳解一次由讀寫鎖引起的內(nèi)存泄漏

作者:Coder的技術(shù)之路 2021-08-02 13:08:56

云計算

虛擬化 JVM相關的異常,一直是一線研發(fā)比較頭疼的問題。因為對于業(yè)務代碼,JVM的運行基本算是黑盒,當異常發(fā)生時,較難直觀的看到和找到問題所在,這也是我們一直要研究其內(nèi)部邏輯的原因。

創(chuàng)新互聯(lián)服務項目包括海曙網(wǎng)站建設、海曙網(wǎng)站制作、海曙網(wǎng)頁制作以及海曙網(wǎng)絡營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關系等,向廣大中小型企業(yè)、政府機構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,海曙網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟效益。目前,我們服務的客戶以成都為中心已經(jīng)輻射到海曙省份的部分城市,未來相信會繼續(xù)擴大服務區(qū)域并繼續(xù)獲得客戶的支持與信任!

JVM相關的異常,一直是一線研發(fā)比較頭疼的問題。因為對于業(yè)務代碼,JVM的運行基本算是黑盒,當異常發(fā)生時,較難直觀的看到和找到問題所在,這也是我們一直要研究其內(nèi)部邏輯的原因。

本篇就由一個近期線上JVM內(nèi)存泄漏的例子,帶大家強行分析一波~

Part1 線上服務器報警了

某天,同事來找我?guī)兔Γ瓉硎悄诚到y(tǒng)毫無征兆的來了一連串報警,一波機器的老年代內(nèi)存占用率超過閾值~

1.1先看表現(xiàn)

老年代內(nèi)存占用

可以看到,在7月中旬之前,內(nèi)存占用還是比較正常的,每次GC都可以回收掉很大一部分的老年代對象。

而中旬之后,老年代內(nèi)存一直緩慢增長而無法釋放。很明顯,應該是對象沒法被正?;厥諏е?。

內(nèi)存泄漏了~

1.2 怎么辦呢

如果是剛上線的項目爆出了此類問題,因為影響面比較小,可以直接先回滾代碼,止血為第一要務。

不過,這個項目明顯已經(jīng)上線N多天,中間還不知道上過多少需求,而且,既然流量近期有上漲導致問題出現(xiàn),說明,已經(jīng)對客開流量了。

回滾是不可能了,抓緊時間定位問題,上線修復吧。

Part2 定位問題

一般的步驟:

  • 拿到dump文件
  • 用MAT等工具,找出內(nèi)存占用過多的異常對象,以及引用關系
  • 分析異常對象關聯(lián)代碼的可能問題

不過,因為這次dump下來的文件十多G,太大的,MAT基本無能為力,只能打印出來人工分析了

2.1 定位問題代碼

jmap結(jié)果查看

很幸運,異常對象非常明顯。Point對象和GeoDispLocal對象,居然多達好幾百萬實例數(shù),那就先看下代碼中這兩個對象是怎么用的。

  
 
 
 
  1. private static final CacheMap> NEAR_DISTRICT_CACHE = new CacheMap>(3600 * 1000, 1000);
  2. private static final CacheMap LOCAL_POINT_CACHE = new CacheMap(3600 * 1000, 6000);

都是被存放在本次緩存CacheMap中(內(nèi)存泄漏的一個常見原因,就是因為被靜態(tài)集合持有,無法回收導致),而dump文件中的CacheMap.Entry也是非常高的。

CacheMap就是我們的第一優(yōu)先懷疑對象了。先看下這個緩存類是怎么回事:

  
 
 
 
  1. ublic class CacheMap {
  2.     private final long expireMs;
  3.     private LRUMap> valueMap;
  4.     //其他略
  5. }

內(nèi)部依賴一個帶LRU功能的map,怎么實現(xiàn)的呢:

  
 
 
 
  1. public class LRUMap extends LinkedHashMap {
  2.     private static final long serialVersionUID = 1L;
  3.     private final int maxCapacity;
  4.     // 這個map不會擴容
  5.     private static final float LOAD_FACTOR = 0.99f;
  6.     private final ReadWriteLock lock = new ReentrantReadWriteLock();
  7.     public LRUMap(int maxCapacity) {
  8.         super(maxCapacity, LOAD_FACTOR, true);
  9.         this.maxCapacity = maxCapacity;
  10.     }
  11.     @Override
  12.     protected boolean removeEldestEntry(java.util.Map.Entry eldest) {
  13.         return size() > maxCapacity;
  14.     }
  15.     @Override
  16.     public V get(Object key) {
  17.         try {
  18.             lock.readLock().lock();
  19.             return super.get(key);
  20.         } finally {
  21.             lock.readLock().unlock();
  22.         }
  23.     }
  24.     @Override
  25.     public V put(K key, V value) {
  26.         try {
  27.             lock.writeLock().lock();
  28.             return super.put(key, value);
  29.         } finally {
  30.             lock.writeLock().unlock();
  31.         }
  32.     }
  33.     //remove clear 略
  34. }

內(nèi)部是一個依賴LinkedHashMap實現(xiàn)的LRU緩存??醋⑨?,目的是要構(gòu)建一個限定容量、且不會進行擴容的MAP(百度了一波,和網(wǎng)上的實現(xiàn)一模一樣~)。那么,實際情況真的和想象中的一樣么?。

2.2 LinkedHashMap實現(xiàn)的LRUMap好使么

我們來看容量和擴容相關的設置:為什么設計者認為該LRUMap不會進行擴容?

  
 
 
 
  1. //**把容量和擴容相關的參數(shù)摘出來**
  2. //用戶期望的最大容量
  3. private final int maxCapacity;
  4. //加載系數(shù)
  5. private static final float LOAD_FACTOR = 0.99f;
  6. //構(gòu)造函數(shù)中調(diào)用LinkedHashMap進行初始化
  7. super(maxCapacity, LOAD_FACTOR, true);
  8. @Override  //復寫刪除最久元素條件方法
  9. protected boolean removeEldestEntry(java.util.Map.Entry eldest) {
  10.    //當LinkedHashMap.size 比 我們限定容量大時,執(zhí)行刪除
  11.    return size() > maxCapacity;
  12. }

按我們的實際使用實例化一下:

  • maxCapacity=6000,是我們希望的最大元素容量。
  • load_factor=0.99 加載因子。
  • Map內(nèi)部threshold=8192*0.99=8110,是那么下次擴容時的容量大小。(map中table容量的真實大小是離6000最近的2的N次冪,即8192)。

因為復寫了LRU條件函數(shù),當size>6000時會進行LRU替換。因此,理論上,size永遠不會達到8110。

怎么解決并發(fā)下的讀寫沖突呢?

  
 
 
 
  1. //讀寫鎖
  2. private final ReadWriteLock lock = new ReentrantReadWriteLock();
  3.  
  4. public V get(Object key) {
  5.    try {
  6.        lock.readLock().lock();
  7.        return super.get(key);
  8.    } finally {
  9.        lock.readLock().unlock();
  10.    }
  11. }
  12. public V put(K key, V value) {
  13.    try {
  14.       lock.writeLock().lock();
  15.       return super.put(key, value);
  16.    } finally {
  17.       lock.writeLock().unlock();
  18.    }
  19. }

設計者為了解決并發(fā)下的讀寫沖突,給查詢和修改方法加了鎖,為了兼顧性能,使用了讀寫鎖:在get的時候加讀鎖,在put/remove的時候加寫鎖。

看起來,整個設計很好的解決了LRUMap的固定容量和并發(fā)操作問題,那么事實是什么樣的呢?

其實,這個問題很早就有人分析過了[1] ,是因為LinkedHashMap在get讀操作的時候,會為了維護LRU從而進行元素修改,即將get到的元素轉(zhuǎn)移到鏈表最后。這樣,就導致了讀寫并發(fā)問題,但這個解釋感覺朦朦朧朧,因此,我決定在其基礎上對讀寫并發(fā)問題再講細致一些。

2.3 LinkedHashMap內(nèi)存泄漏拆解

都加了讀寫鎖為什么不好使呢?

這里我們還是需要先明確,讀寫鎖的概念和適用場景:讀寫鎖,允許多個線程共享讀鎖,適用于讀多寫少的情況。(前提是,讀操作不會改變存儲結(jié)構(gòu))

所以,問題就發(fā)生在get操作上,LinkedHashMap的get操作被重寫,目的是為了實現(xiàn)LRU功能,在get之后,將當前節(jié)點移動到鏈表最后。

移動啊,同志們,這明顯是一個寫操作,所以,加讀鎖還有用么?

即允許多線程進入,又進行了修改,那還能起什么作用,能沒有并發(fā)問題么?

下面,對照節(jié)點移動的代碼,詳細拆解一下多線程下的并發(fā)問題:

get之后的節(jié)點移動,將節(jié)點移動到最后

實際拆解分析如下,為什么在多線程的情況下,會出現(xiàn)內(nèi)存泄漏:

時間片下多線程的get執(zhí)行

我們看到,在線程1執(zhí)行完前兩句,讓出了時間片,當線程2執(zhí)行到p.after=null之后又出讓了時間片,這樣,本來a應該是后面的<2,B>節(jié)點,結(jié)果多線程下變成了null,最終,后面兩個節(jié)點被踢出了鏈表,刪除操作無法觸達,造成內(nèi)存泄漏。

驗證的代碼就不貼了,大家有興趣可以自己試一下~

Part3 總結(jié)

話說回來,既然定位到了問題,這個內(nèi)存泄漏怎么修復呢?

可以把讀寫鎖改成互斥鎖?;蛘咧苯佑梅植际酱鎯Γ苈嗌倌?,是不是,既方便,簡單,又免得為了節(jié)約機器內(nèi)存自己構(gòu)造LRUMap。

每一個八股文都不只是為了面試,而是每次線上問題排查的基石。千萬別把八股文的作用定位錯了。。。

本文轉(zhuǎn)載自微信公眾號「Coder的技術(shù)之路  」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系Coder的技術(shù)之路公眾號。


當前文章:高并發(fā)服務優(yōu)化篇:詳解一次由讀寫鎖引起的內(nèi)存泄漏
當前路徑:http://uogjgqi.cn/article/djpdjso.html
掃二維碼與項目經(jīng)理溝通

我們在微信上24小時期待你的聲音

解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流