2.垃圾收集器与内存分配策略
生活随笔
收集整理的這篇文章主要介紹了
2.垃圾收集器与内存分配策略
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
2.1 概述
垃圾收集需要考慮三件事:哪些內存需要回收;什么時候回收;如何回收;經過半個世紀的發展,今天的內存動態分配和內存回收技術都已經十分成熟,但我們為什么要了解垃圾收集和內存分配呢?答案呼之欲出,當需要排查各種內存溢出,內存泄漏問題時,當垃圾收集成為系統達到更高并發量的瓶頸時,我們就必須對這些“自動化”的技術實施必要的監控和調節。其中,程序計數器,虛擬機棧,本地方法棧都隨著線程而生,線程而滅,棧中的棧幀隨著方法的進入和退出而有條不紊的入棧和出棧,每一個棧幀中分配多少內存基本上在類結構確定下來就已知的,因此這幾個區域內存的分配和回收都是確定的,當方法結束或者線程結束,這三個區域內存自然就跟著回收了。
但是,堆和方法區有著顯著的不確定性,一個接口的多個實現類需要的內存可能不同,一個方法所執行的把不同條件分支所需要的內存也可能不一樣,只有在運行期間,我們才知道程序究竟會創建哪些對象,創建多少個對象,這部分內存的分配和回收都是動態的。
2.2 對象已死
2.2.1 引用計數法
內容:
在對象中添加一個引用計數器,每當有一個地方引用它,計數器就加一;當引用失效時,計數器就減一;在任何時刻計數器為零的對象是不可能再被使用的。問題:
雖然這個算法實現簡單,判定效率很高,但是存在著循環引用的問題。當A->B,B->A,除此之外沒有任何引用,實際上這兩個對象已經不可能再被訪問了,但是因為它們互相引用著對方,導致它們的引用計數器不為零,無法回收。2.2.2 可達性分析算法
內容:
通過GC Roots的根對象作為起始節點集,從這些節點開始,根據引用關系向下搜索,搜索過程所走的路徑稱為引用鏈,如果某個對象到GC Roots間沒有任何引用鏈相連,則證明此對象是不可能在被使用的。GC Roots對象包括:
在虛擬機棧中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數,局部變量,臨時變量等;在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量;
在方法區中常量引用的對象,譬如字符串常量池里的引用;
在本地方法棧中Native方法引用的對象;
虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象等,還有系統類加載器;
所有被同步鎖持有的對象;
2.2.3 再談引用
將引用分為強引用,軟引用,弱引用,虛引用。強引用:Object obj = new Object(),無論任何情況,只要強引用關系還存在,垃圾收集器就永遠不會回收掉被引用的對象;
軟引用:在系統中將要發生內存溢出異常前,會把這些對象列進回收范圍之中進行第二次回收;
弱引用:當垃圾收集器開始工作,無論當前內存是否充足,都會回收掉只被弱引用關聯的對象;
虛引用:一個對象是否有虛引用的存在,完全不影響其生存時間。
2.2.4 生存還是死亡
即使在可達性分析算法中判定為不可達的對象,但它還有一次自我拯救的機會,如果此對象有必要執行finalize()方法,那么會將它放置在F-Queue的隊列之中,并在稍后由Finalizer線程去執行finalize()方法,如果它成功逃脫,則將它移除“即將回收”的集合,如果它沒有逃脫,那么它真的要被回收了。2.2.5 回收方法區
方法區的垃圾收集只要回收兩部分內容:廢棄的常量和不再使用的類型。2.3 垃圾收集算法
2.3.1 分代收集理論
內容:
它雖名為理論,但實際它是一套符合大多數程序運行實際情況的經驗法則,它建立在兩個假說之上:(1)弱分代假說:絕大多數對象都是朝生夕滅的。
(2)強分代假說:熬過越多次垃圾收集的過程就越難消滅掉。
這兩個分代假說共同奠定了多款常用的垃圾收集器一致的設計原則:收集器應該將Java堆劃分出不同的區域,然后將回收對象依據其年齡分配到不同的區域之中存儲。
把分代收集理論放在虛擬機里,設計者會將Java堆劃分為新生代(Young Generation)和老年代(Old Generation)兩個區域,在新生代中,每次垃圾收集時都發現大批對象死去,而每次回收后存活的對象將會逐步晉升到老年大中存放。但是分代收集并非簡單劃分一下內存區域這么容易,它存在一個明顯問題,那就是對象不是孤立存在的,對象之間存在跨代引用。假設要進行一次只局限于新生代區域內的收集,但新生代完全有可能被老年代所引用,為了找出該區域中存活對象,不得不再額外遍歷整個老年代中所有對象來確保可達性分析結果的正確性。這樣做,無疑是為內存回收帶來很大的性能負擔。因此,需要第三條經驗法則:
(3)跨代引用假說:跨代引用相對于同代引用僅占極少數。
根據這條假說,我們就不應再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用,只需在新生代建立一個全局的數據結構(記憶集),這個結構把老年代劃分為若干小塊,標識出老年代的哪一塊內存存在跨代引用。此后當個發生Minor GC時,只有包含了跨代引用的小塊內存才會被GC Roots進行掃描。
回收類型:
部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,其中分為:新生代收集(Minor GC):指目標只是新生代的垃圾收集。
老年代收集(Major GC):指目標只是老年代的垃圾收集。
混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。
整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。
回收算法:
標記-復制,標記-清除,標記-整理2.3.2 標記-清除算法
內容:
首先標記出所有需要回收的對象,在標記完成后,統一回收掉所有被標記的對象,也可以反過來,標記存活對象,統一回收所有未被標記的對象。缺點:
(1)執行效率不穩定:執行效率隨對象數量的增長而降低;(2)內存空間碎片化問題:標記清除之后會產生大量的不連續內存碎片;
2.3.3 標記-復制算法
內容:
把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內存都是用Eden和其中一塊Survivor。發生垃圾搜集時,將Eden和Survivor中仍然存活的對象一次性全部復制到另一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間。缺點:
HotSpot虛擬機默認將Eden,Survivor和另一塊Survivor的大小比例是8:1:1,由于無法保證每次垃圾收集時都有高于90%的對象被收集,所以,為了安全設計,當Survivor空間不足容納一次Minor GC之后存活的對象,就需要依賴老年區進行分配擔保。2.3.4 標記-整理算法
內容:
首先標記出所有需要回收的對象,在標記完成后,讓所有存活的對象都想內存空間一端移動,然后直接清理掉邊界以外的內存。缺點:
對于老年代這種每次回收都有大量對象存活的區域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行,這種停頓叫做”Stop The World“。2.4 經典垃圾收集器
2.4.1 Serial收集器
這個收集器是一個單線程收集器,“單線程”強調的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直至它收集結束。“Stop The World”雖然看起來很酷,但是這項工作其實是由后臺自動發起和自動完成的,對于很多應用來說,正常工作的線程全部停掉是不能接受的。雖然它現在面臨的狀態是食之無味,棄之可惜,但是它也有著優于其他收集器的地方,那就是簡單高效,對于內存資源受限的環境,它是所有收集器里額外內存消耗最小的;對于單核處理器或者處理器核心數較少的環境來說,它沒有線程的交互開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。
2.4.2 ParNew收集器
它是Serial收集器的多線程并行版本,除了同時使用多條線程進行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數,收集算法,Stop The World,對象分配規則,回收策略等都與Serial完全一致。在單核心處理器的環境中,ParNew收集器絕對比不上Serial,而且,在超線程技術實現的偽雙核處理器環境中都不能百分之百超越Serial收集器。
2.4.3 Parallel Scavenge收集器
它是能夠并行收集的多線程收集器,它的諸多特性從表面上看和ParNew非常相似。但是它也有自己的特點,對于CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量
2.4.4 Serial Old收集器
它是Serial收集器的老年代版本,同樣是單線程收集器,采用標記-整理算法。2.4.5 Parallel Old收集器
它是Parallel Scavenge收集器的老年代版本,支持多線程并發收集,基于標記-整理算法實現。2.4.6 CMS收集器
它是一種以獲取最短回收停頓時間為目標的收集器,它是基于標記-清除算法實現的;整個收集過程分為四個步驟:(1)初始標記:僅僅只是標記一下GC Roots能直接關聯到的對象;
(2)并發標記:從GC Roots的直接關聯的對象開始遍歷整個對象圖的過程;
(3)重新標記:為了修正并發并發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄;
(4)并發清除:清理刪除掉標記階段判斷的已經死亡的對象。
雖然它已經很成功了,但是它存在三個明顯的缺點:
(1)CMS收集器對處理器資源非常敏感,在并發階段,雖然不會導致用戶線程停頓,但是由于占用了一部分線程而導致應用程序變慢,降低吞吐量。
(2)由于CMS收集器無法處理浮動垃圾,有可能出現“Concurrent Mode Failure”失敗進而導致另一次完全“Stop The World”的Full GC的產生。
(3)因為CMS是基于標記-清除算法,因此會產生大量空間碎片,空間碎片過多,將會給大對象分配帶來麻煩,往往出現老年代還有很多剩余空間,但就是無法找到足夠大的連續空間來分配當前對象,不得不觸發Full GC的情況。
2.4.7 G1收集器
它開創了收集器面向局部收集的設計思路和基于Region的內存布局形式。如果想要建立起“停頓時間模型”的收集器,首先要有一個思想的轉變,在之前的全部垃圾收集器,垃圾收集的目標范圍要么是整個新生代(Minor GC),要么是整個老年代(Major GC),再要么就是整個Java堆(Full GC),而G1跳出了這個樊籠,它可以面向堆內存任何部分來組成回收集進行回收,衡量標準不再是它屬于哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1的Mixed GC模式。雖然G1也仍然遵循分代收集理論設計的,但它不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要扮演新生代Eden空間,Survivor空間,或者老年代空間。
Region中還有一類特殊的存放大對象的Humongous區域,G1大多數行為把這塊區域作為老年代的一部分進行看待。
G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,這樣可以有計劃地避免在整個Java堆中進行全區域垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個Region里面的垃圾堆積的價值大小,價值即回收所獲得的空間大小以及回收所需時間經驗值,然后在后臺維護一個優先級列表,每次根據設定的允許收集停頓時間,優先處理回收價值收益最大的Region。
G1收集器的運作過程大致分為四個步驟:
(1)初始標記:僅僅只是標記一下GC Roots能直接關聯到的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發運行時,能正確地在可用地Region中分配新對象;
(2)并發標記:從GC Roots開始對堆中對象進行可達性分析,遞歸掃描整個堆中的對象圖,找到要回收的對象,當對象圖掃描完成以后,還要重新處理SATB記錄下的在并發時有引用變動的的對象;
(3)最終標記:對用戶線程做另一個短暫的暫停,用于處理并發階段結束后仍遺留下來的最后那少量的SATB記錄;(需要Stop The World)
(4)篩選回收:負責更新Region的統計數據,對各個Region的回收價值和成本排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回憶集,然后把決定回收的那一部分Region的存活對象復制到空的Region中,再清理掉整個舊Region的全部空間。
2.5 實戰:內存分配與回收策略
2.5.1 對象優先在Eden分配
大多情況下,對象在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。2.5.2 大對象直接進入老年代
大對象就是指需要大量連續內存空間的Java對象,最經典的就是很長的字符串,或者元素數量很龐大的數組。2.5.3 長期存活的對象將進入老年代
為了做到判斷哪些對象應該放在新生代,哪些對象應該放在老年代,虛擬機給每個對象定義一個對象年齡計數器,存放在對象頭。對象通常在Eden區里誕生,如果經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,該對象會被移動到Survivor空間中,并且將其對象年齡設為1歲。對象在Survivor區中每熬過以此Minor GC,年齡就加一,當它的年齡到達默認15,就會晉升到老年代中。2.5.4 動態對象年齡判定
為了適應不同程度內存狀況,HotSpot虛擬機并不是達到設定才晉升老年代的,如果Survivor空間中相同年齡所有對象大小的總和大于Survivor空間一半,年齡大于或者等于該年齡的對象就可以直接進入老年代無需等到設定的年齡。2.6 HotSpot的算法細節實現
2.6.1 并發的可達性分析
引入三色標記,按照“是否訪問過”這個條件標記成一下三個顏色:(1)白色:表示對象尚未被垃圾收集器訪問過。
(2)黑色:表示對象已經被垃圾收集器訪問過,且這個對象的所有引用都已經掃描過了。
(3)灰色:表示對象已經被垃圾收集器訪問過,但這個對象上至少存在一個引用還沒有被掃描過。
由于并發,收集器在對象圖上標記顏色,同時用戶線程在修改引用關系,可能出現兩種后果。一種是把原本消亡的對象錯誤標記為存活,雖然不是好事,但其實可以容忍,下次收集清理掉就好。另一種原本存活的對象錯誤標記為已消亡,這非常致命,程序會發生錯誤,下面是致命的產生過程:
當且僅當以下兩個條件同時滿足時,產生“對象消亡”的問題:
(1)賦值器插入了一條或多條從黑色對象到白色對象的新引用;
(2)賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用;
為了解決這個問題,兩種解決方案:
(1)增量更新:當黑色對象插入新的指向白色對象的引用關系時,就將這個新插入的引用記錄下來,等并發掃描結束后,再將這些記錄過的引用關系中的黑色對象為根,重新掃描一次。
(2)原始快照:當灰色對象要刪除指向白色對象的引用關系時,就將這個要刪除的引用記錄下來,在并發掃描結束后,再將這些記錄過的引用關系中灰色對象為根,重新掃描一次。
總結
以上是生活随笔為你收集整理的2.垃圾收集器与内存分配策略的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android——横幅通知
- 下一篇: (二)PUN 2基本教程