<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          詳解一次由讀寫(xiě)鎖引起的內(nèi)存泄漏

          共 6525字,需瀏覽 14分鐘

           ·

          2021-08-05 17:01

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

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

          Part1線上服務(wù)器報(bào)警了

          某天,同事來(lái)找我?guī)兔Γ瓉?lái)是某系統(tǒng)毫無(wú)征兆的來(lái)了一連串報(bào)警,一波機(jī)器的老年代內(nèi)存占用率超過(guò)閾值~

          1.1先看表現(xiàn)

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

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

          而中旬之后,老年代內(nèi)存一直緩慢增長(zhǎng)而無(wú)法釋放。很明顯,應(yīng)該是對(duì)象沒(méi)法被正常回收導(dǎo)致。

          內(nèi)存泄漏了~

          1.2怎么辦呢

          如果是剛上線的項(xiàng)目爆出了此類問(wèn)題,因?yàn)橛绊懨姹容^小,可以直接先回滾代碼,止血為第一要?jiǎng)?wù)。

          不過(guò),這個(gè)項(xiàng)目明顯已經(jīng)上線N多天,中間還不知道上過(guò)多少需求,而且,既然流量近期有上漲導(dǎo)致問(wèn)題出現(xiàn),說(shuō)明,已經(jīng)對(duì)客開(kāi)流量了。

          回滾是不可能了,抓緊時(shí)間定位問(wèn)題,上線修復(fù)吧。

          Part2定位問(wèn)題

          一般的步驟:

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

          不過(guò),因?yàn)檫@次dump下來(lái)的文件十多G,太大的,MAT基本無(wú)能為力,只能打印出來(lái)人工分析了

          2.1定位問(wèn)題代碼

          jmap結(jié)果查看

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

          private static final CacheMap<String, List<GeoDispLocal>> NEAR_DISTRICT_CACHE = new CacheMap<String, List<GeoDispLocal>>(3600 * 10001000);

          private static final CacheMap<Integer, Point> LOCAL_POINT_CACHE = new CacheMap<Integer, Point>(3600 * 10006000);

          都是被存放在本次緩存CacheMap中(內(nèi)存泄漏的一個(gè)常見(jiàn)原因,就是因?yàn)楸混o態(tài)集合持有,無(wú)法回收導(dǎo)致),而dump文件中的CacheMap.Entry也是非常高的。

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

          public class CacheMap<KV{
              private final long expireMs;
              private LRUMap<K, CacheMap.Entry<V>> valueMap;
              //其他略
          }

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

          public class LRUMap<KVextends LinkedHashMap<KV{
              private static final long serialVersionUID = 1L;
              private final int maxCapacity;
              // 這個(gè)map不會(huì)擴(kuò)容
              private static final float LOAD_FACTOR = 0.99f;
              private final ReadWriteLock lock = new ReentrantReadWriteLock();

              public LRUMap(int maxCapacity) {
                  super(maxCapacity, LOAD_FACTOR, true);
                  this.maxCapacity = maxCapacity;
              }

              @Override
              protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
                  return size() > maxCapacity;
              }

              @Override
              public V get(Object key) {
                  try {
                      lock.readLock().lock();
                      return super.get(key);
                  } finally {
                      lock.readLock().unlock();
                  }
              }

              @Override
              public V put(K key, V value) {
                  try {
                      lock.writeLock().lock();
                      return super.put(key, value);
                  } finally {
                      lock.writeLock().unlock();
                  }
              }
              //remove clear 略
          }

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

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

          我們來(lái)看容量和擴(kuò)容相關(guān)的設(shè)置:為什么設(shè)計(jì)者認(rèn)為該LRUMap不會(huì)進(jìn)行擴(kuò)容?

          //**把容量和擴(kuò)容相關(guān)的參數(shù)摘出來(lái)**
          //用戶期望的最大容量
          private final int maxCapacity;
          //加載系數(shù)
          private static final float LOAD_FACTOR = 0.99f;
          //構(gòu)造函數(shù)中調(diào)用LinkedHashMap進(jìn)行初始化
          super(maxCapacity, LOAD_FACTOR, true);

          @Override  //復(fù)寫(xiě)刪除最久元素條件方法
          protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
             //當(dāng)LinkedHashMap.size 比 我們限定容量大時(shí),執(zhí)行刪除
             return size() > maxCapacity;
          }

          按我們的實(shí)際使用實(shí)例化一下:

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

          因?yàn)閺?fù)寫(xiě)了LRU條件函數(shù),當(dāng)size>6000時(shí)會(huì)進(jìn)行LRU替換。因此,理論上,size永遠(yuǎn)不會(huì)達(dá)到8110。

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

          //讀寫(xiě)鎖
          private final ReadWriteLock lock = new ReentrantReadWriteLock();
           
          public V get(Object key) {
             try {
                 lock.readLock().lock();
                 return super.get(key);
             } finally {
                 lock.readLock().unlock();
             }
          }

          public V put(K key, V value) {
             try {
                lock.writeLock().lock();
                return super.put(key, value);
             } finally {
                lock.writeLock().unlock();
             }
          }

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

          看起來(lái),整個(gè)設(shè)計(jì)很好的解決了LRUMap的固定容量和并發(fā)操作問(wèn)題,那么事實(shí)是什么樣的呢?

          其實(shí),這個(gè)問(wèn)題很早就有人分析過(guò)了[1] ,是因?yàn)長(zhǎng)inkedHashMap在get讀操作的時(shí)候,會(huì)為了維護(hù)LRU從而進(jìn)行元素修改,即將get到的元素轉(zhuǎn)移到鏈表最后。這樣,就導(dǎo)致了讀寫(xiě)并發(fā)問(wèn)題,但這個(gè)解釋感覺(jué)朦朦朧朧,因此,我決定在其基礎(chǔ)上對(duì)讀寫(xiě)并發(fā)問(wèn)題再講細(xì)致一些。

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

          都加了讀寫(xiě)鎖為什么不好使呢?

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

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

          移動(dòng)啊,同志們,這明顯是一個(gè)寫(xiě)操作,所以,加讀鎖還有用么?

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

          下面,對(duì)照節(jié)點(diǎn)移動(dòng)的代碼,詳細(xì)拆解一下多線程下的并發(fā)問(wèn)題:

          get之后的節(jié)點(diǎn)移動(dòng),將節(jié)點(diǎn)移動(dòng)到最后

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

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

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

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

          Part3總結(jié)

          話說(shuō)回來(lái),既然定位到了問(wèn)題,這個(gè)內(nèi)存泄漏怎么修復(fù)呢?

          可以把讀寫(xiě)鎖改成互斥鎖。或者直接用分布式存儲(chǔ),能慢多少呢,是不是,既方便,簡(jiǎn)單,又免得為了節(jié)約機(jī)器內(nèi)存自己構(gòu)造LRUMap。

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

          參考資料

          [1]

          LinkedHashMap引發(fā)的內(nèi)存泄漏: "https://blog.csdn.net/yejingtao703/article/details/108062262"


          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www.99在线观看 | 成年人观看黄色视频 | 老色鬼久久综合 | 在线成人中文字幕无码影 | 天天av免费 |