<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 為什么不建議使用自定義Object作為HashMap的key?

    此前部門內的一個線上系統上線后內存一路飆高、一段時間后直接占滿。協助開發人員去分析定位,發現內存中某個Object的量遠遠超出了預期的范圍,很明顯出現內存泄漏了。

    結合代碼分析發現,泄漏的這個對象,主要存在一個全局HashMap中,是作為HashMap的Key值。第一反應就是這里key對應類沒有去覆寫equals()和hashCode()方法,但對照代碼仔細一看卻發現其實已經按要求提供了自定義的equals和hashCode方法了。進一步走讀業務實現邏輯,才發現了其中的玄機。

    踩坑歷程回顧

    鑒于項目代碼相對保密,這里舉個簡單的DEMO來輔助說明下。

    場景:
    內存中構建一個HashMap<User, List<Post>>映射集,用于存儲每個用戶最近的發帖信息(只是個例子,實際工作中如果遇到這種用戶發帖緩存的場景,一般都是用的集中緩存,而不是單機緩存)。

    用戶信息User類定義如下:

    @Data
    public class User {
        // 用戶名稱
        private String userName;
        // 賬號ID
        private String accountId;
        // 用戶上次登錄時間,每次登錄的時候會自動更新DB對應時間
        private long lastLoginTime;
        // 其他字段,忽略
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            User user = (User) o;
            return lastLoginTime == user.lastLoginTime &&
                    Objects.equals(userName, user.userName) &&
                    Objects.equals(accountId, user.accountId);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(userName, accountId, lastLoginTime);
        }
    }
    
    

    實際使用的時候,用戶發帖之后,會將這個帖子信息添加到用戶對應的緩存中。

    
    /**
     *  將發帖信息加入到用戶緩存中
     *
     * @param currentUser 當前用戶
     * @param postContent 帖子信息
     */
    public void addCache(User currentUser, Post postContent) {
        cache.computeIfAbsent(currentUser, k -> new ArrayList<>()).add(postContent);
    }
    
    

    當實際運行的時候,會發現問題就來了,Map中的記錄越來越多,遠超系統內實際的用戶數量。為什么呢?仔細看下User類就可以知道了!

    原來編碼的時候直接用IDE工具自動生成的equals和hashCode方法,里面將lastLoginTime也納入計算邏輯了。這樣每次用戶重新登錄之后,對應hashCode值也就變了,這樣發帖的時候判斷用戶是不存在Map中的,就會再往map中插入一條,隨著時間的推移,內存中數據就會越來越多,導致內存泄漏。

    這么一看,其實問題很簡單。但是實際編碼的時候,很多人往往又會忽略這些細節、或者當時可能沒有這個場景,后面維護的人新增了點邏輯,就會出問題 —— 說白了,就是埋了個坑給后面的人踩上了。

    hashCode覆寫的講究

    hashCode,即一個Object的散列碼。HashCode的作用:

    • 對于List、數組等集合而言,HashCode用途不大;
    • 對于HashMap\HashTable\HashSet等集合而言,HashCode有很重要的價值。

    HashCode在上述HashMap等容器中主要是用于尋域,即尋找某個對象在集合中的區域位置,用于提升查詢效率。

    一個Object對象往往會存在多個屬性字段,而選擇什么屬性來計算hashCode值,具有一定的考驗:

    • 如果選擇的字段太多,而HashCode()在程序執行中調用的非常頻繁,勢必會影響計算性能;
    • 如果選擇的太少,計算出來的HashCode勢必很容易就會出現重復了。

    為什么hashCode和equals要同時覆寫

    這就與HashMap的底層實現邏輯有關系了。

    對于JDK1.8+版本中,HashMap底層的數據結構形如下圖所示,使用數組+鏈表或者紅黑樹的結構形式:

    給定key進行查詢的時候,分為2步:

    1. 調用key對象的hashCode()方法,獲取hashCode值,然后換算為對應數組的下標,找到對應下標位置;
    2. 根據hashCode找到的數組下標可能會同時對應多個key(所謂的hash碰撞,不同元素產生了相同的hashCode值),這個時候使用key對象提供的equals()方法,進行逐個元素比對,直到找到相同的元素,返回其所對應的值。

    根據上面的介紹,可以概括為:

    • hashCode負責大概定位,先定位到對應片區
    • equals負責在定位的片區內,精確找到預期的那一個

    這里也就明白了為什么hashCode()和equals()需要同時覆寫。

    數據退出機制的兜底

    其實,說到這里,全局Map出現內存泄漏,還有一點就是編碼實現的時候缺少對數據退出機制的考慮。
    參考下redis之類的依賴內存的緩存中間件,都有一個繞不開的兜底策略,即數據淘汰機制。

    對于業務類編碼實現的時候,如果使用Map等容器類來實現全局緩存的時候,應該要結合實際部署情況,確定內存中允許的最大數據條數,并提供超出指定容量時的處理策略。比如我們可以基于LinkedHashMap來定制一個基于LRU策略的緩存Map,來保證內存數據量不會無限制增長,這樣即使代碼出問題也只是這一個功能點出問題,不至于讓整個進程宕機。

    
    public class FixedLengthLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
        private static final long serialVersionUID = 1287190405215174569L;
        private int maxEntries;
    
        public FixedLengthLinkedHashMap(int maxEntries, boolean accessOrder) {
            super(16, 0.75f, accessOrder);
            this.maxEntries = maxEntries;
        }
        
        /**
         *  自定義數據淘汰觸發條件,在每次put操作的時候會調用此方法來判斷下
         */
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > maxEntries;
        }
    }
    
    

    總結

    梳理下幾個要點:

    • 最好不要使用Object作為HashMap的Key
    • 如果不得已必須要使用,除了要覆寫equals和hashCode方法
    • 覆寫的equals和hashCode方法中一定不能有頻繁易變更的字段
    • 內存緩存使用的Map,最好對Map的數據記錄條數做一個強制約束,提供下數據淘汰策略。

    好啦,關于這個問題的分享就到這里咯,你是否有在工作中遇到此類相同或者相似的問題呢?歡迎一起分享討論下哦~


    我是悟道,聊技術、又不僅僅聊技術~

    如果覺得有用,請點個關注,也可以關注下我的公眾號【架構悟道】,獲取更及時的更新。

    期待與你一起探討,一起成長為更好的自己。

    posted @ 2022-06-29 15:11  架構悟道  閱讀(960)  評論(12編輯  收藏  舉報
    国产美女a做受大片观看