咱們從頭到(dào)尾說(shuō)一次 Java 垃圾回收 - 新聞資訊 - 雲南小程序開發|雲南軟件開發|雲南網站建設-昆明融晨信息技術有限公司

159-8711-8523

雲南網建設/小程序開發/軟件開發

知識

不(bù)管是(shì)網站,軟件還是(shì)小程序,都要(yào / yāo)直接或間接能爲(wéi / wèi)您産生價值,我們在(zài)追求其視覺表現的(de)同時(shí),更側重于(yú)功能的(de)便捷,營銷的(de)便利,運營的(de)高效,讓網站成爲(wéi / wèi)營銷工具,讓軟件能切實提升企業内部管理水平和(hé / huò)效率。優秀的(de)程序爲(wéi / wèi)後期升級提供便捷的(de)支持!

您當前位置>首頁 » 新聞資訊 » 技術分享 >

咱們從頭到(dào)尾說(shuō)一次 Java 垃圾回收

發表時(shí)間:2019-7-23

發布人(rén):融晨科技

浏覽次數:30

640?wx_fmt=gif

640?wx_fmt=jpeg

作者 | 率鴿

責編 | 胡雪蕊

之(zhī)前上(shàng)學的(de)時(shí)候有這(zhè)個(gè)一個(gè)梗,說(shuō)在(zài)食堂裏吃飯,吃完把餐盤端走清理的(de),是(shì) C++ 程序員,吃完直接就(jiù)走的(de),是(shì) Java 程序員。?

确實,在(zài) Java 的(de)世界裏,似乎我們不(bù)用對垃圾回收那麽的(de)專注,很多初學者不(bù)懂 GC,也(yě)依然能寫出(chū)一個(gè)能用甚至還不(bù)錯的(de)程序或系統。但其實這(zhè)并不(bù)代表 Java 的(de) GC 就(jiù)不(bù)重要(yào / yāo)。相反,它是(shì)那麽的(de)重要(yào / yāo)和(hé / huò)複雜,以(yǐ)至于(yú)出(chū)了(le/liǎo)問題,那些初學者除了(le/liǎo)打開 GC 日志,看着一堆0101的(de)天文,啥也(yě)做不(bù)了(le/liǎo)。?

今天我們就(jiù)從頭到(dào)尾完整地(dì / de)聊一聊 Java 的(de)垃圾回收。

640?wx_fmt=png

什麽是(shì)垃圾回收

垃圾回收(Garbage Collection,GC),顧名思義就(jiù)是(shì)釋放垃圾占用的(de)空間,防止内存洩露。有效的(de)使用可以(yǐ)使用的(de)内存,對内存堆中已經死亡的(de)或者長時(shí)間沒有使用的(de)對象進行清除和(hé / huò)回收。

Java 語言出(chū)來(lái)之(zhī)前,大(dà)家都在(zài)拼命的(de)寫 C 或者 C++ 的(de)程序,而(ér)此時(shí)存在(zài)一個(gè)很大(dà)的(de)矛盾,C++ 等語言創建對象要(yào / yāo)不(bù)斷的(de)去開辟空間,不(bù)用的(de)時(shí)候又需要(yào / yāo)不(bù)斷的(de)去釋放控件,既要(yào / yāo)寫構造函數,又要(yào / yāo)寫析構函數,很多時(shí)候都在(zài)重複的(de) allocated,然後不(bù)停的(de)析構。于(yú)是(shì),有人(rén)就(jiù)提出(chū),能不(bù)能寫一段程序實現這(zhè)塊功能,每次創建,釋放控件的(de)時(shí)候複用這(zhè)段代碼,而(ér)無需重複的(de)書寫呢?

1960年,基于(yú) MIT 的(de) Lisp 首先提出(chū)了(le/liǎo)垃圾回收的(de)概念,而(ér)這(zhè)時(shí) Java 還沒有出(chū)世呢!所以(yǐ)實際上(shàng) GC 并不(bù)是(shì)Java的(de)專利,GC 的(de)曆史遠遠大(dà)于(yú) Java 的(de)曆史!

640?wx_fmt=png

怎麽定義垃圾

既然我們要(yào / yāo)做垃圾回收,首先我們得搞清楚垃圾的(de)定義是(shì)什麽,哪些内存是(shì)需要(yào / yāo)回收的(de)。

引用計數算法

引用計數算法(Reachability Counting)是(shì)通過在(zài)對象頭中分配一個(gè)空間來(lái)保存該對象被引用的(de)次數(Reference Count)。如果該對象被其它對象引用,則它的(de)引用計數加1,如果删除對該對象的(de)引用,那麽它的(de)引用計數就(jiù)減1,當該對象的(de)引用計數爲(wéi / wèi)0時(shí),那麽該對象就(jiù)會被回收。

 

先創建一個(gè)字符串,這(zhè)時(shí)候"jack"有一個(gè)引用,就(jiù)是(shì) m。

640?wx_fmt=png

然後将 m 設置爲(wéi / wèi) null,這(zhè)時(shí)候"jack"的(de)引用次數就(jiù)等于(yú)0了(le/liǎo),在(zài)引用計數算法中,意味着這(zhè)塊内容就(jiù)需要(yào / yāo)被回收了(le/liǎo)。

 

640?wx_fmt=png

引用計數算法是(shì)将垃圾回收分攤到(dào)整個(gè)應用程序的(de)運行當中了(le/liǎo),而(ér)不(bù)是(shì)在(zài)進行垃圾收集時(shí),要(yào / yāo)挂起整個(gè)應用的(de)運行,直到(dào)對堆中所有對象的(de)處理都結束。因此,采用引用計數的(de)垃圾收集不(bù)屬于(yú)嚴格意義上(shàng)的(de)"Stop-The-World"的(de)垃圾收集機制。

看似很美好,但我們知道(dào)JVM的(de)垃圾回收就(jiù)是(shì)"Stop-The-World"的(de),那是(shì)什麽原因導緻我們最終放棄了(le/liǎo)引用計數算法呢?看下面的(de)例子(zǐ)。

 

1. 定義2個(gè)對象

2. 相互引用
3. 置空各自的(de)聲明引用

640?wx_fmt=png

我們可以(yǐ)看到(dào),最後這(zhè)2個(gè)對象已經不(bù)可能再被訪問了(le/liǎo),但由于(yú)他(tā)們相互引用着對方,導緻它們的(de)引用計數永遠都不(bù)會爲(wéi / wèi)0,通過引用計數算法,也(yě)就(jiù)永遠無法通知GC收集器回收它們。

可達性分析算法

可達性分析算法(Reachability Analysis)的(de)基本思路是(shì),通過一些被稱爲(wéi / wèi)引用鏈(GC Roots)的(de)對象作爲(wéi / wèi)起點,從這(zhè)些節點開始向下搜索,搜索走過的(de)路徑被稱爲(wéi / wèi)(Reference Chain),當一個(gè)對象到(dào) GC Roots 沒有任何引用鏈相連時(shí)(即從 GC Roots 節點到(dào)該節點不(bù)可達),則證明該對象是(shì)不(bù)可用的(de)。

640?wx_fmt=png

通過可達性算法,成功解決了(le/liǎo)引用計數所無法解決的(de)問題-“循環依賴”,隻要(yào / yāo)你無法與 GC Root 建立直接或間接的(de)連接,系統就(jiù)會判定你爲(wéi / wèi)可回收對象。那這(zhè)樣就(jiù)引申出(chū)了(le/liǎo)另一個(gè)問題,哪些屬于(yú) GC Root。

Java 内存區域

在(zài) Java 語言中,可作爲(wéi / wèi) GC Root 的(de)對象包括以(yǐ)下4種:

  • 虛拟機棧(棧幀中的(de)本地(dì / de)變量表)中引用的(de)對象

  • 方法區中類靜态屬性引用的(de)對象

  • 方法區中常量引用的(de)對象

  • 本地(dì / de)方法棧中 JNI(即一般說(shuō)的(de) Native 方法)引用的(de)對象

640?wx_fmt=png

虛拟機棧(棧幀中的(de)本地(dì / de)變量表)中引用的(de)對象

此時(shí)的(de) s,即爲(wéi / wèi) GC Root,當s置空時(shí),localParameter 對象也(yě)斷掉了(le/liǎo)與 GC Root 的(de)引用鏈,将被回收。

 

方法區中類靜态屬性引用的(de)對象

s 爲(wéi / wèi) GC Root,s 置爲(wéi / wèi) null,經過 GC 後,s 所指向的(de) properties 對象由于(yú)無法與 GC Root 建立關系被回收。

而(ér) m 作爲(wéi / wèi)類的(de)靜态屬性,也(yě)屬于(yú) GC Root,parameter 對象依然與 GC root 建立着連接,所以(yǐ)此時(shí) parameter 對象并不(bù)會被回收。

 

方法區中常量引用的(de)對象

m 即爲(wéi / wèi)方法區中的(de)常量引用,也(yě)爲(wéi / wèi) GC Root,s 置爲(wéi / wèi) null 後,final 對象也(yě)不(bù)會因沒有與 GC Root 建立聯系而(ér)被回收。

 

本地(dì / de)方法棧中引用的(de)對象

任何 Native 接口都會使用某種本地(dì / de)方法棧,實現的(de)本地(dì / de)方法接口是(shì)使用 C 連接模型的(de)話,那麽它的(de)本地(dì / de)方法棧就(jiù)是(shì) C 棧。當線程調用 Java 方法時(shí),虛拟機會創建一個(gè)新的(de)棧幀并壓入 Java 棧。然而(ér)當它調用的(de)是(shì)本地(dì / de)方法時(shí),虛拟機會保持 Java 棧不(bù)變,不(bù)再在(zài)線程的(de) Java 棧中壓入新的(de)幀,虛拟機隻是(shì)簡單地(dì / de)動态連接并直接調用指定的(de)本地(dì / de)方法。

640?wx_fmt=png

640?wx_fmt=png

怎麽回收垃圾

在(zài)确定了(le/liǎo)哪些垃圾可以(yǐ)被回收後,垃圾收集器要(yào / yāo)做的(de)事情就(jiù)是(shì)開始進行垃圾回收,但是(shì)這(zhè)裏面涉及到(dào)一個(gè)問題是(shì):如何高效地(dì / de)進行垃圾回收。由于(yú)Java虛拟機規範并沒有對如何實現垃圾收集器做出(chū)明确的(de)規定,因此各個(gè)廠商的(de)虛拟機可以(yǐ)采用不(bù)同的(de)方式來(lái)實現垃圾收集器,這(zhè)裏我們讨論幾種常見的(de)垃圾收集算法的(de)核心思想。

标記 --- 清除算法

640?wx_fmt=png

标記清除算法(Mark-Sweep)是(shì)最基礎的(de)一種垃圾回收算法,它分爲(wéi / wèi)2部分,先把内存區域中的(de)這(zhè)些對象進行标記,哪些屬于(yú)可回收标記出(chū)來(lái),然後把這(zhè)些垃圾拎出(chū)來(lái)清理掉。就(jiù)像上(shàng)圖一樣,清理掉的(de)垃圾就(jiù)變成未使用的(de)内存區域,等待被再次使用。

這(zhè)邏輯再清晰不(bù)過了(le/liǎo),并且也(yě)很好操作,但它存在(zài)一個(gè)很大(dà)的(de)問題,那就(jiù)是(shì)内存碎片。

上(shàng)圖中等方塊的(de)假設是(shì) 2M,小一些的(de)是(shì) 1M,大(dà)一些的(de)是(shì) 4M。等我們回收完,内存就(jiù)會切成了(le/liǎo)很多段。我們知道(dào)開辟内存空間時(shí),需要(yào / yāo)的(de)是(shì)連續的(de)内存區域,這(zhè)時(shí)候我們需要(yào / yāo)一個(gè) 2M的(de)内存區域,其中有2個(gè) 1M 是(shì)沒法用的(de)。這(zhè)樣就(jiù)導緻,其實我們本身還有這(zhè)麽多的(de)内存的(de),但卻用不(bù)了(le/liǎo)。

複制算法

640?wx_fmt=png

複制算法(Copying)是(shì)在(zài)标記清除算法上(shàng)演化而(ér)來(lái),解決标記清除算法的(de)内存碎片問題。它将可用内存按容量劃分爲(wéi / wèi)大(dà)小相等的(de)兩塊,每次隻使用其中的(de)一塊。當這(zhè)一塊的(de)内存用完了(le/liǎo),就(jiù)将還存活着的(de)對象複制到(dào)另外一塊上(shàng)面,然後再把已使用過的(de)内存空間一次清理掉。保證了(le/liǎo)内存的(de)連續可用,内存分配時(shí)也(yě)就(jiù)不(bù)用考慮内存碎片等複雜情況,邏輯清晰,運行高效。

上(shàng)面的(de)圖很清楚,也(yě)很明顯的(de)暴露了(le/liǎo)另一個(gè)問題,合着我這(zhè)140平的(de)大(dà)三房,隻能當70平米的(de)小兩房來(lái)使?代價實在(zài)太高。

标記整理算法

640?wx_fmt=png

标記整理算法(Mark-Compact)标記過程仍然與标記 --- 清除算法一樣,但後續步驟不(bù)是(shì)直接對可回收對象進行清理,而(ér)是(shì)讓所有存活的(de)對象都向一端移動,再清理掉端邊界以(yǐ)外的(de)内存區域。

标記整理算法一方面在(zài)标記-清除算法上(shàng)做了(le/liǎo)升級,解決了(le/liǎo)内存碎片的(de)問題,也(yě)規避了(le/liǎo)複制算法隻能利用一半内存區域的(de)弊端。看起來(lái)很美好,但從上(shàng)圖可以(yǐ)看到(dào),它對内存變動更頻繁,需要(yào / yāo)整理所有存活對象的(de)引用地(dì / de)址,在(zài)效率上(shàng)比複制算法要(yào / yāo)差很多。

分代收集算法分代收集算法(Generational Collection)嚴格來(lái)說(shuō)并不(bù)是(shì)一種思想或理論,而(ér)是(shì)融合上(shàng)述3種基礎的(de)算法思想,而(ér)産生的(de)針對不(bù)同情況所采用不(bù)同算法的(de)一套組合拳。對象存活周期的(de)不(bù)同将内存劃分爲(wéi / wèi)幾塊。一般是(shì)把 Java 堆分爲(wéi / wèi)新生代和(hé / huò)老年代,這(zhè)樣就(jiù)可以(yǐ)根據各個(gè)年代的(de)特點采用最适當的(de)收集算法。在(zài)新生代中,每次垃圾收集時(shí)都發現有大(dà)批對象死去,隻有少量存活,那就(jiù)選用複制算法,隻需要(yào / yāo)付出(chū)少量存活對象的(de)複制成本就(jiù)可以(yǐ)完成收集。而(ér)老年代中因爲(wéi / wèi)對象存活率高、沒有額外空間對它進行分配擔保,就(jiù)必須使用标記-清理或者标記 --- 整理算法來(lái)進行回收。so,另一個(gè)問題來(lái)了(le/liǎo),那内存區域到(dào)底被分爲(wéi / wèi)哪幾塊,每一塊又有什麽特别适合什麽算法呢?

640?wx_fmt=png

内存模型與回收策略

640?wx_fmt=png

Java 堆(Java Heap)是(shì)JVM所管理的(de)内存中最大(dà)的(de)一塊,堆又是(shì)垃圾收集器管理的(de)主要(yào / yāo)區域,這(zhè)裏我們主要(yào / yāo)分析一下 Java 堆的(de)結構。

Java 堆主要(yào / yāo)分爲(wéi / wèi)2個(gè)區域-年輕代與老年代,其中年輕代又分 Eden 區和(hé / huò) Survivor 區,其中 Survivor 區又分 From 和(hé / huò) To 2個(gè)區。可能這(zhè)時(shí)候大(dà)家會有疑問,爲(wéi / wèi)什麽需要(yào / yāo) Survivor 區,爲(wéi / wèi)什麽Survivor 還要(yào / yāo)分2個(gè)區。不(bù)着急,我們從頭到(dào)尾,看看對象到(dào)底是(shì)怎麽來(lái)的(de),而(ér)它又是(shì)怎麽沒的(de)。

Eden 區

IBM 公司的(de)專業研究表明,有将近98%的(de)對象是(shì)朝生夕死,所以(yǐ)針對這(zhè)一現狀,大(dà)多數情況下,對象會在(zài)新生代 Eden 區中進行分配,當 Eden 區沒有足夠空間進行分配時(shí),虛拟機會發起一次 Minor GC,Minor GC 相比 Major GC 更頻繁,回收速度也(yě)更快。

通過 Minor GC 之(zhī)後,Eden 會被清空,Eden 區中絕大(dà)部分對象會被回收,而(ér)那些無需回收的(de)存活對象,将會進到(dào) Survivor 的(de) From 區(若 From 區不(bù)夠,則直接進入 Old 區)。

Survivor 區

Survivor 區相當于(yú)是(shì) Eden 區和(hé / huò) Old 區的(de)一個(gè)緩沖,類似于(yú)我們交通燈中的(de)黃燈。Survivor 又分爲(wéi / wèi)2個(gè)區,一個(gè)是(shì) From 區,一個(gè)是(shì) To 區。每次執行 Minor GC,會将 Eden 區和(hé / huò) From 存活的(de)對象放到(dào) Survivor 的(de) To 區(如果 To 區不(bù)夠,則直接進入 Old 區)。

爲(wéi / wèi)啥需要(yào / yāo)?

不(bù)就(jiù)是(shì)新生代到(dào)老年代麽,直接 Eden 到(dào) Old 不(bù)好了(le/liǎo)嗎,爲(wéi / wèi)啥要(yào / yāo)這(zhè)麽複雜。想想如果沒有 Survivor 區,Eden 區每進行一次 Minor GC,存活的(de)對象就(jiù)會被送到(dào)老年代,老年代很快就(jiù)會被填滿。而(ér)有很多對象雖然一次 Minor GC 沒有消滅,但其實也(yě)并不(bù)會蹦跶多久,或許第二次,第三次就(jiù)需要(yào / yāo)被清除。這(zhè)時(shí)候移入老年區,很明顯不(bù)是(shì)一個(gè)明智的(de)決定。

所以(yǐ),Survivor 的(de)存在(zài)意義就(jiù)是(shì)減少被送到(dào)老年代的(de)對象,進而(ér)減少 Major GC 的(de)發生。Survivor 的(de)預篩選保證,隻有經曆16次 Minor GC 還能在(zài)新生代中存活的(de)對象,才會被送到(dào)老年代。

爲(wéi / wèi)啥需要(yào / yāo)倆?

設置兩個(gè) Survivor 區最大(dà)的(de)好處就(jiù)是(shì)解決内存碎片化。

我們先假設一下,Survivor 如果隻有一個(gè)區域會怎樣。Minor GC 執行後,Eden 區被清空了(le/liǎo),存活的(de)對象放到(dào)了(le/liǎo) Survivor 區,而(ér)之(zhī)前 Survivor 區中的(de)對象,可能也(yě)有一些是(shì)需要(yào / yāo)被清除的(de)。問題來(lái)了(le/liǎo),這(zhè)時(shí)候我們怎麽清除它們?在(zài)這(zhè)種場景下,我們隻能标記清除,而(ér)我們知道(dào)标記清除最大(dà)的(de)問題就(jiù)是(shì)内存碎片,在(zài)新生代這(zhè)種經常會消亡的(de)區域,采用标記清除必然會讓内存産生嚴重的(de)碎片化。因爲(wéi / wèi) Survivor 有2個(gè)區域,所以(yǐ)每次 Minor GC,會将之(zhī)前 Eden 區和(hé / huò) From 區中的(de)存活對象複制到(dào) To 區域。第二次 Minor GC 時(shí),From 與 To 職責兌換,這(zhè)時(shí)候會将 Eden 區和(hé / huò) To 區中的(de)存活對象再複制到(dào) From 區域,以(yǐ)此反複。

這(zhè)種機制最大(dà)的(de)好處就(jiù)是(shì),整個(gè)過程中,永遠有一個(gè) Survivor space 是(shì)空的(de),另一個(gè)非空的(de) Survivor space 是(shì)無碎片的(de)。那麽,Survivor 爲(wéi / wèi)什麽不(bù)分更多塊呢?比方說(shuō)分成三個(gè)、四個(gè)、五個(gè)?顯然,如果 Survivor 區再細分下去,每一塊的(de)空間就(jiù)會比較小,容易導緻 Survivor 區滿,兩塊 Survivor 區可能是(shì)經過權衡之(zhī)後的(de)最佳方案。

Old 區

老年代占據着2/3的(de)堆内存空間,隻有在(zài) Major GC 的(de)時(shí)候才會進行清理,每次 GC 都會觸發“Stop-The-World”。内存越大(dà),STW 的(de)時(shí)間也(yě)越長,所以(yǐ)内存也(yě)不(bù)僅僅是(shì)越大(dà)就(jiù)越好。由于(yú)複制算法在(zài)對象存活率較高的(de)老年代會進行很多次的(de)複制操作,效率很低,所以(yǐ)老年代這(zhè)裏采用的(de)是(shì)标記 --- 整理算法。

除了(le/liǎo)上(shàng)述所說(shuō),在(zài)内存擔保機制下,無法安置的(de)對象會直接進到(dào)老年代,以(yǐ)下幾種情況也(yě)會進入老年代。

大(dà)對象

大(dà)對象指需要(yào / yāo)大(dà)量連續内存空間的(de)對象,這(zhè)部分對象不(bù)管是(shì)不(bù)是(shì)“朝生夕死”,都會直接進到(dào)老年代。這(zhè)樣做主要(yào / yāo)是(shì)爲(wéi / wèi)了(le/liǎo)避免在(zài) Eden 區及2個(gè) Survivor 區之(zhī)間發生大(dà)量的(de)内存複制。當你的(de)系統有非常多“朝生夕死”的(de)大(dà)對象時(shí),得注意了(le/liǎo)。

長期存活對象

虛拟機給每個(gè)對象定義了(le/liǎo)一個(gè)對象年齡(Age)計數器。正常情況下對象會不(bù)斷的(de)在(zài) Survivor 的(de) From 區與 To 區之(zhī)間移動,對象在(zài) Survivor 區中每經曆一次 Minor GC,年齡就(jiù)增加1歲。當年齡增加到(dào)15歲時(shí),這(zhè)時(shí)候就(jiù)會被轉移到(dào)老年代。當然,這(zhè)裏的(de)15,JVM 也(yě)支持進行特殊設置。

動态對象年齡

虛拟機并不(bù)重視要(yào / yāo)求對象年齡必須到(dào)15歲,才會放入老年區,如果 Survivor 空間中相同年齡所有對象大(dà)小的(de)總合大(dà)于(yú) Survivor 空間的(de)一半,年齡大(dà)于(yú)等于(yú)該年齡的(de)對象就(jiù)可以(yǐ)直接進去老年區,無需等你“成年”。

這(zhè)其實有點類似于(yú)負載均衡,輪詢是(shì)負載均衡的(de)一種,保證每台機器都分得同樣的(de)請求。看似很均衡,但每台機的(de)硬件不(bù)通,健康狀況不(bù)同,我們還可以(yǐ)基于(yú)每台機接受的(de)請求數,或每台機的(de)響應時(shí)間等,來(lái)調整我們的(de)負載均衡算法。

作者簡介:聶小龍(花名:率鴿),阿裏巴巴高級開發工程師。本文系作者投稿,版權歸作者所有。

2019年人(rén)工智能系統學:

https://edu.csdn.net/topic/ai30?utm_source=csdn_bw

【END】

640?wx_fmt=jpeg

熱 文 推 薦

?華爲(wéi / wèi) 5G 手機 8 月上(shàng)市;百度回應“業務轉向”;微軟上(shàng)線 Python 教程 | 極客頭條

?全棧開發者意味着什麽?

?大(dà)數據時(shí)代已來(lái),開發者該如何出(chū)擊?

?真實揭秘 90 後程序員婚戀現狀,有點紮心!

“Hyperledger Fabric 是(shì)假區塊鏈!”

AutoML前沿技術與實踐經驗分享 | 免費報名

?知識體系、算法題、教程、面經,這(zhè)是(shì)一份超贊的(de)AI資源列表

SaaS前世今生:老樹開新花

?中國(guó)第一程序員,微軟得不(bù)到(dào)他(tā)就(jiù)要(yào / yāo)毀了(le/liǎo)他(tā)!

640?wx_fmt=png你點的(de)每個(gè)“在(zài)看”,我都認真當成了(le/liǎo)喜歡

相關案例查看更多