<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>

          一個hashCode問題的追問,差點讓我陷入無底洞

          共 4940字,需瀏覽 10分鐘

           ·

          2021-01-19 12:45

          • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

          • If you can NOT explain it simply, you do NOT understand it well enough

          現(xiàn)陸續(xù)將Demo代碼和技術(shù)文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star??

          起因

          起因是群里的一位童鞋突然問了這么問題:

          如果重寫 equals 不重寫 hashcode 會有什么影響?

          這個問題從上午10:45 開始陸續(xù)討論,到下午15:39 接近尾聲。

          這是一個好問題,更是一個高頻基礎(chǔ)面試題。

          隨著討論的進(jìn)行,問題慢慢集中在內(nèi)存溢出和內(nèi)存泄漏的問題上

          內(nèi)存溢出 VS 內(nèi)存泄漏

          這兩個詞在中文解釋上有些相似,至少給我的第一感覺,他們的差別是這樣的(有人和我一樣嗎?)

          內(nèi)存溢出:Out of Memory (OOM) ,這個大家都很熟悉了,理解起來也很簡單,就是內(nèi)存不夠用了(啤酒【對象】太多,杯子【內(nèi)存】裝不下了)

          那啥是內(nèi)存泄漏呢?

          內(nèi)存泄漏:Memory Leak

          特意查了一下 Leak 的字典含義,解釋1的直白翻譯是【通常是由于錯誤失誤,從一個開口 進(jìn)入或逃脫】

          所以程序中的內(nèi)存泄漏我的理解更多是:由于程序的編寫錯誤暴漏出一些 開口,導(dǎo)致一些對象進(jìn)入這寫開口,最終導(dǎo)致相關(guān)問題,進(jìn)一步說白了,程序有漏洞,不當(dāng)?shù)恼{(diào)用就會出問題

          所以接下來我們主要來看看 Java 內(nèi)存泄漏,以及問題的起因 hashCode 和內(nèi)存泄漏到底有哪些關(guān)系

          內(nèi)存泄漏

          咱也是一個有身份證的人,不能總講大白話,相對官方的內(nèi)存泄漏解釋是這樣滴:

          內(nèi)存泄漏說明的是這樣一種情況:堆中存在一些不再使用的對象,但垃圾收集器無法將它們從內(nèi)存中刪除(垃圾收集器定期刪除未引用的對象,但從不收集仍在引用的對象),因此對它們進(jìn)行了不必要的維護(hù)

          這句話略顯抽象,一張圖你就能明白

          如果有用的、但垃圾收集器又不能刪除的對象增多,就像下圖這樣,那么就會逐漸導(dǎo)致內(nèi)存溢出(OOM)了

          所以也可以總結(jié)為,OOM 的原因之一可能是內(nèi)存泄漏導(dǎo)致的

          內(nèi)存泄漏會帶來哪些問題

          內(nèi)存泄漏,會導(dǎo)致真正可用內(nèi)存變少,在沒達(dá)到 OOM 的這個過程中,就會出現(xiàn)奇奇怪怪的問題

          1. 當(dāng)應(yīng)用程序長時間連續(xù)運行時,性能會嚴(yán)重下降,畢竟可用內(nèi)存變小
          2. 自發(fā)的和奇怪的應(yīng)用程序崩潰
          3. 應(yīng)用程序偶爾會耗盡連接對象(這個經(jīng)常聽說吧)
          4. 最終的結(jié)果是 OOM

          所以也可以反過來推理,如果發(fā)生上述問題,有可能程序的某些地方發(fā)生了內(nèi)存泄漏

          那常見的哪些情形可能會引起內(nèi)存泄漏呢?又有哪些解決辦法呢?

          會引起內(nèi)存泄漏的常見情形與相應(yīng)解決辦法

          靜態(tài)成員變量的亂用

          直接來看一個例子

          @Slf4j
          public?class?StaticTest?{
          ?public?static?List?list?=?new?ArrayList<>();

          ?public?void?populateList()?{
          ??for?(int?i?=?0;?i?10000000;?i++)?{
          ???list.add(Math.random());
          ??}
          ?}

          ?public?static?void?main(String[]?args)?{
          ??new?StaticTest().populateList();
          ?}
          }

          populateList() 是一個 public 方法,可能被各種調(diào)用,導(dǎo)致 list 無限增大

          解決辦法

          解決辦法很簡單,針對這種情形(也就是通常所說的長周期對象引用短周期對象),就是將 list 放到方法內(nèi)部,方法棧幀執(zhí)行完自動就會被回收了

          public?void?populateList()?{
          ???List?list?=?new?ArrayList<>();
          ???for?(int?i?=?0;?i?10000000;?i++)?{
          ??????list.add(Math.random());
          ???}
          }

          有童鞋可能有疑問:

          看 Spring 源碼時有好多是 static 修飾的成員變量,難道它們也會導(dǎo)致內(nèi)存泄漏?

          不是的,如果你仔細(xì)看邏輯,它們都是是在容器初始化的過程中一次性加載的,所以不會像 populateList 隨著調(diào)用次數(shù)的增加,無限撐大 List

          未關(guān)閉的流

          在學(xué)習(xí)流的時候老師就在耳邊反復(fù)說:

          一定要關(guān)閉流... 閉流... ... ?... ...

          因為每當(dāng)我們建立一個新的連接或打開一個流時(比如數(shù)據(jù)庫連接、輸入流和會話對象),JVM都會為這些資源分配內(nèi)存,如果不關(guān)閉,這就是占用空間"有用"的對象, GC 就不會回收他們,當(dāng)請求很大,來個請求就新建一個流,最終都還沒關(guān)閉,結(jié)果可想而知

          解決辦法

          流的解決辦法很簡單,其實主要遵循相應(yīng)范式就可以避免此類問題

          1. 通過 try/catch/finally范式在 finally 關(guān)掉流
          2. 如果你用的 Java 7+ 的版本,也可以用 try-with-resources, 這樣代碼在編譯后會自動幫你關(guān)閉流
          3. 也可以使用 Lombok 的 @Cleanup 注解, 就像下面這樣
          @Cleanup?InputStream?jobJarInputStream?=?new?URL(jobJarUrl).openStream();
          @Cleanup?OutputStream?jobJarOutputStream?=?new?FileOutputStream(jobJarFile);
          IOUtils.copy(jobJarInputStream,?jobJarOutputStream);

          不正確的 equals 和 hashCode 實現(xiàn)

          又回到了這兩個函數(shù)上,有很大一部分程序員不會主動重寫 equals 和 hashCode,尤其是用 Lombok @Data 注解(該注解默認(rèn)會幫助重寫這兩個函數(shù))后,更會忽視這兩個方法實現(xiàn),一不小心的使就可能引起內(nèi)存泄漏

          來看個非常簡單的例子:

          public?class?MemLeakTest?{

          ???public?static?void?main(String[]?args)?throws?InterruptedException?{
          ??????Map?map?=?new?HashMap<>();
          ??????Person?p1?=?new?Person("zhangsan",?1);
          ??????Person?p2?=?new?Person("zhangsan",?1);
          ??????Person?p3?=?new?Person("zhangsan",?1);

          ??????map.put(p1,?"zhangsan");
          ??????map.put(p2,?"zhangsan");
          ??????map.put(p3,?"zhangsan");

          ??????System.out.println(map.entrySet().size());?//?運行結(jié)果:3
          ???}
          }??

          @Getter
          @Setter
          class?Person?{
          ?private?String?name;
          ?private?Integer?id;

          ?public?Person(String?name,?Integer?id){
          ??this.name?=?name;
          ??this.id?=?id;
          ?}
          }

          Person 類沒有重寫 hashCode 方法,那 Map 的 put 方法就會調(diào)用 Object 默認(rèn)的 hashCode 方法

          public?V?put(K?key,?V?value)?{
          ????return?putVal(hash(key),?key,?value,?false,?true);
          }

          static?final?int?hash(Object?key)?{
          ??int?h;
          ??return?(key?==?null)???0?:?(h?=?key.hashCode())?^?(h?>>>?16);
          }

          p1, p2, p3 在【業(yè)務(wù)】屬性上是完全相同的三個對象,由于「對象地址」的不同導(dǎo)致生成的 hashCode 不一樣,最終都被放到 Map 中,這就會導(dǎo)致業(yè)務(wù)重復(fù)對象占用空間,所以這也是內(nèi)存泄漏的一種

          解決辦法

          解決辦法很簡單,在 Person 上加一個 Lombok 的 @Data 注解自動幫你重寫 hashCode 方法,或手動在 IDE 中 generate,再次運行,結(jié)果就為 1了,符合業(yè)務(wù)需求

          那重寫了 hashCode 確實可以避免重復(fù)對象的加入,那這就完事大吉了嗎, 再來看個例子

          public?static?void?main(String[]?args)?throws?InterruptedException?{
          ??//?注意:?HashSet?的底層也是?Map?結(jié)構(gòu)?
          ??Set?set?=?new?HashSet();

          ???Person?p1?=?new?Person("zhangsan",?1);
          ???Person?p2?=?new?Person("lisi",?2);
          ???Person?p3?=?new?Person("wanger",?3);

          ???set.add(p1);
          ???set.add(p2);
          ???set.add(p3);
          ???
          ???System.out.println(set.size());?//?運行結(jié)果:3
          ???p3.setName("wangermao");
          ???set.remove(p3);
          ???System.out.println(set.size());?//?運行結(jié)果:3
          ???set.add(p3);
          ???System.out.println(set.size());?//?運行結(jié)果:4
          }

          從運行結(jié)果中來看,很顯然 set.remove(p3) 沒有刪除成功,因為 p3.setName("wangermao") 后,重新計算 p3 的 hashCode 會發(fā)生變化,所以 remove 的時候會找不到相應(yīng)的 Node,這就又給了增加相同對象的“機(jī)會”,導(dǎo)致業(yè)務(wù)中無用的對象被引用著,所以可以說這也是內(nèi)存泄漏的一種。運行結(jié)果來看:

          所以諸如此類操作,最好是先 remove,然后更改屬性,最后再重新 add 進(jìn)去

          看到這,你應(yīng)該發(fā)現(xiàn)了,要解決 hashCode 相關(guān)的問題,你要充分了解集合的特性,更要留意類是否重寫了該方法以及它們的實現(xiàn)方式,避免出現(xiàn)內(nèi)存泄漏情況

          ThreadLocal

          群消息中的最后,小姐姐 留下【ThreadLocal】幾個字,深藏功與名的離開了,一看就是高手

          ThreadLocal 是面試多線程的高頻考點,它的好處是可以快速方便的做到線程隔離,但大家也都知道他是一把雙刃劍,因為使用不好就有可能導(dǎo)致內(nèi)存泄漏了

          實際工作中我們都是使用線程池來管理線程,這種方式可以讓線程得到反復(fù)利用(故意不讓 GC 回收),

          現(xiàn)在,如果任何類創(chuàng)建了一個ThreadLocal變量,但沒有顯式地刪除它,那么即使在web應(yīng)用程序停止之后,該對象的副本仍將保留在工作線程中,從而阻止了該對象被垃圾收集,所以亂用也會導(dǎo)致內(nèi)存泄漏

          解決辦法

          解決辦法依舊很簡單,依舊是遵循標(biāo)準(zhǔn)

          1. 調(diào)用 ThreadLocal 的 remove() 方法,移除當(dāng)前線程變量值
          2. 也可以將它看作一種 resource,使用 try/finally 范式,萬一在運行過程中出現(xiàn)異常,還可以在 finally 中 remove 掉
          try?{
          ????threadLocal.set(System.nanoTime());
          ????//?business?code
          }
          finally?{
          ????threadLocal.remove();
          }

          我覺得小姐姐一定是高手

          總的來說,引起內(nèi)存泄漏的原因非常多,比如還有引用外部類的內(nèi)部類等問題,這里不再展開說明,只是說明了幾種非常常見的可能引發(fā)內(nèi)存泄漏問題的幾種場景

          內(nèi)存泄漏問題不易察覺,所以有時需要借助工具來幫忙

          JVisualVM

          JVisualvm 【可視化JVM】,可分析JDK1.6及其以上版本的JVM運行時JVM參數(shù)系統(tǒng)參數(shù)堆棧CPU使用等信息。可分析本地應(yīng)用及遠(yuǎn)程應(yīng)用,在JDK1.6以上版本中自帶,工具的使用暫不展開說明, 想快速使用此工具,只需要在 IDE 中安裝個 VisualVM Launcher 插件


          然后在進(jìn)行基本的配置

          然后在IDE的右上角或當(dāng)前類鼠標(biāo)右鍵就可以點擊運行查看了

          運行起 VisualVM 就是這樣子了

          不要走,還沒結(jié)束,在總結(jié)這篇文章的時候,我還發(fā)現(xiàn)了「新大陸」

          HashCode 真是根據(jù)對象內(nèi)存地址生成的?

          腦海中的印象不知道為何,很根深蒂固的接受了Object hashCode 是根據(jù)對象內(nèi)存地址生成的,這次剛好想探求一下 hashCode 的本質(zhì),還著實打破了我的固有印象 (以 JDK1.8 為例)

          OpenJDK 定義 hashCode 的方法在下面兩個文件中

          • src/share/vm/prims/jvm.h
          • src/share/vm/prims/jvm.cpp

          逐步看下去,最終會來到 get_next_hash 這個方法中,方便大家查看我先把方法截圖至此:

          總的來說有 6 種生成 hashCode 的方式:

          • 0: A randomly generated number
          • 1: A function of memory address of the object
          • 2: A hardcoded 1 (used for sensitivity testing.)
          • 3: A sequence.
          • 4: The memory address of the object, cast to int
          • 5(else): Thread state combined with xor-shift[1]

          那在 JDK1.8 種用的哪一種呢?

          ![](https://rgyb.sunluomeng.top/Screen Shot 2020-08-01 at 1.35.29 PM.png)

          可以看到在 JDK1.8 中生成 hashCode 的方式是 5, 也就是走程序的 else 路徑,即使用 Xorshift,并不是之前認(rèn)為的對象內(nèi)存地址「1」,以為老版本是采用對象內(nèi)存地址的方式,所以繼續(xù)查看其他版本

          從圖中可以看出,JDK1.6[2]JDK1.7[3] 版本生成 hashCode 的方式「1」隨機(jī)數(shù)的形式,和我們原本認(rèn)為的并不一樣,別的版本沒有繼續(xù)查詢,至于「流傳下來」說是對象內(nèi)存地址生成的 hashCode 我也木有再深入研究,有了解的同學(xué)還請留言賜教

          那么問題來了:

          假設(shè)用的 JDK1.6或 JDK1.7,它們生成 hashCode 的方式是隨機(jī)生成的,那一個對象多次調(diào)用hashCode是會有不同的hashCode 呢?(排除服務(wù)重啟的情況)

          顯然應(yīng)該不會的,因為如果每次都變化, 存儲到集合中的對象那就很容易丟失了,那問題又來了:

          它們存在哪了?

          hash 值是存在對象頭中的,我們還知道對象頭中還可能存儲線程ID,所以他們在某些情形中還會存在沖突

          對象頭中 hashCode 和 偏向鎖的沖突

          jvm 啟動時,可以使用 -XX:+UseBiasedLocking=true 開啟偏向鎖,(關(guān)于偏向鎖,輕量級鎖,重量級鎖大家查閱 synchronized 相關(guān)文檔就可以),這里引 OpenJDK Wiki[4] 里面的圖片加以文字說明整個沖突過程

          所以,調(diào)用 Object 的 hashCode() 方法或者 System.identityHashCode() 方法會讓對象不能使用偏向鎖。到這里你也就應(yīng)該知道了,如果你還想使用偏向鎖,那最好重寫 hashCode() 方法,避免使偏向鎖失效

          總結(jié)

          為了解決群的這個問題,發(fā)現(xiàn)新大陸的同時也差點讓我掉入【追問無底洞】,不過通過本文你應(yīng)該了解內(nèi)存溢出和內(nèi)存泄漏的差別,以及他們的解決方案,另外 hashCode[5] 生成方式還著實讓人有些驚訝,如果你知道「hashCode的生成是根據(jù)對象內(nèi)存地址生成的來源,還請留言賜教」。除此之外,小小的 hashCode 還有可能讓偏向鎖失效,所有的這些細(xì)節(jié)問題都有可能是導(dǎo)致程序崩潰的坑,所以勿以「惡」小而為之,毋以「善」小而不為,良好的編程習(xí)慣能避免很多問題

          當(dāng)然想要更好的理解內(nèi)存泄漏,當(dāng)然是要更好的理解 GC 機(jī)制,而想要更好的理解 GC,當(dāng)然是更好的理解 JVM,咱們后續(xù)慢慢分析吧

          靈魂追問

          1. 為了清除 ThreadLocal 線程變量值,不用 ThreadLocal.remove() 方法,而是用 ThreadLocal.set(null) 會達(dá)到同樣的效果嗎?
          2. 你曾經(jīng)遇到哪些不易察覺的內(nèi)存泄漏問題呢?

          點個在看,贊??支持我吧
          瀏覽 44
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  全国男人的天堂网站 | 亚洲国产毛片 | av天堂pt | 亚洲首页欧美美女爱爱首页 | 色色婷婷五月 |