3.1java
哪些須要回收算法
何時回收數組
怎麼回收緩存
垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾佔用的空間,防止內存泄露。有效的使用可使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。安全
3.2對象已死數據結構
Java堆中存放着幾乎Java世界中的全部對象,垃圾收集器在回收對象以前須要知道那些對象「還活着」,那些對象「已經死去」(即不可能再被任何途徑使用的對象)。併發
3.2.1引用計數算法性能
給每一個對象添加一個計數器,當有地方引用該對象時計數器加1,當引用失效時計數器減1。用對象計數器是否爲0來判斷對象是否可被回收。缺點:沒法解決循環引用的問題。優化
3.2.2可達性分析算法this
GC ROOT
的對象做爲搜索起始點,經過引用向下搜索,所走過的路徑稱爲引用鏈。經過對象是否有到達引用鏈的路徑來判斷對象是否可被回收
GC ROOT
的對象:
JNI(一般所說的Native方法)
引用的對象
即便在可達性分析算法中不可達的對象,也不是「非死不可」的。
對象在被標記爲不可達以後,若是對象覆蓋了finalize()方法而且該對象尚未調用過finalize(),那麼這個對象會被放入F-Queue隊列中,並在稍後一個由虛擬機創建的、低優先級的Finalize線程中去執行對象的finalize()方法。稍後GC會對F-Queue的對象進行再一次的標記,若是對象的finalize方法中,將對象從新和GC Roots創建了關聯,那麼在第二次標記中就會被移除出「即將回收」的集合。
可是,finalize線程的優先級很低,GC並不保證會等待對象執行完finalize方法以後再去回收,於是想經過finalize方法區拯救對象的作法,並不靠譜。鑑於finalize()方法這種執行的不肯定性,你們其實能夠忘記finalize方法在Java中的存在了,不管何時,都不要使用finalize方法。
若是該對象沒有覆蓋finalize()方法或者已經調用過finalize()方法,GC就會回收該對象。
3.2.5回收方法區
Java虛擬機規範中規定不要求虛擬機在方法區實現垃圾收集,並且在方法區實現垃圾收集性價比確實很低。在堆中,尤爲是新生代,一次垃圾收集能夠回收75%-95%的空間,而永久代的垃圾回收效率遠低於此。
永久代的垃圾收集主要回收兩部分:廢棄常量和無用的類。回收廢棄常量與回收Java堆的對象很是類似。
1.以常量池的字面量的回收爲例,例如字符串「abc」進入常量池,可是當前系統沒有任何一個string對象引用常量池的字符串「abc」,也沒有其餘地方引用這個字面量,若此時發生回收,則「abc」常量將被回收。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。
2.斷定一個類是「無用的」較爲苛刻,需知足三個條件,知足如下三個條件無用類才能夠被回收(僅僅是能夠,是否必然回收虛擬機有其餘參數控制):
3.3垃圾收集算法
分代收集理論和幾種算法思想
從如何斷定對象消亡的角度:垃圾收集算法分爲 引用計數式垃圾收集(Reference Counting GC 直接垃圾收集)和追蹤式垃圾收集(Tracing GC 也被稱爲 間接垃圾收集)
3.3.1 分代收集理論
分代收集創建在兩個分代假說之上:
弱分代假說(Weak Generational Hypothesis):絕大多數對象都是朝生夕滅的。
強分代假說(Strong Generational Hypothesis):熬過越屢次垃圾收集過程的對象就越難以消亡。
跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對於同代引用來講僅佔極少數。
收集器將Java堆劃分到不一樣區域,將回收對象依據其年齡分配到不一樣的區域中存儲。若是一個區域中大多數對象都是朝生夕滅,難以熬過垃圾收集過程的話,那麼把他們集中放在一塊兒,每次回收時值關注保留少許存活而不不是去標記大量將要回收的對象,就能以叫第代價回收到大量的空間,若是剩下的是難以消亡的對象,那麼把他們集中放到一塊兒,虛擬機就能夠以較低的頻率來回收這個區域,想喝酒同時兼顧了來及收集的時間開銷和內存的空間有效利用。
第一種:標記清除 是最經典的垃圾回收算法
它是最基礎的收集算法。
原理:分爲標記和清除兩個階段:首先標記出全部的須要回收的對象,在標記完成之後統一回收全部被標記的對象。
特色:(1)效率問題,大部分對象須要回收時,標記和清除的效率隨對象數量增加而下降;(2)空間的問題,標記清除之後會產生大量不連續的空間碎片,空間碎片太多可能會致使程序運行過程須要分配較大的對象時候,沒法找到足夠連續內存而不得不提早觸發一次垃圾收集。
地方 :適合在老年代進行垃圾回收,好比CMS收集器就是採用該算法進行回收的。
第二種:標記整理
原理:分爲標記和整理兩個階段:首先標記出全部須要回收的對象,讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
特色:不會產生空間碎片,可是整理會花必定的時間。對象移動必須全程暫停用戶應用線程。移動內存回收時複雜,不移動內存分配時複雜,從垃圾收集時間看,不移動停頓時間更短,移動時間長,從程序吞吐量看(賦值器和收集器效率總和),移動對象更划算
地方:適合老年代進行垃圾收集,parallel Old(針對parallel scanvange gc的) gc和Serial old收集器就是採用該算法進行回收的。
第三種:複製算法
原理:它先將可用的內存按容量劃分爲大小相同的兩塊,每次只是用其中的一塊。當這塊內存用完了,就將還存活着的對象複製到另外一塊上面,而後把已經使用過的內存空間一次清理掉。
特色:沒有內存碎片,只要移動堆頂指針,按順序分配內存便可。代價是將內存縮小位原來的一半。內訓中多數對象是存活的,將會產生大量的內存間複製開銷
地方:適合新生代區進行垃圾回收。serial new,parallel new和parallel scanvage
收集器,就是採用該算法進行回收的。
複製算法改進思路:因爲新生代都是朝生夕死的,因此不須要1:1劃份內存空間,能夠將內存劃分爲一塊較大的Eden和兩塊較小的Suvivor空間。每次使用Eden和其中一塊Survivor。當回收的時候,將Eden和Survivor中還活着的對象一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔使用過的Suevivor空間。其中Eden和Suevivor的大小比例是8:1。缺點是須要老年代進行分配擔保,若是第二塊的Survovor空間不夠的時候,須要對老年代進行垃圾回收,而後存儲新生代的對象,這些新生代固然會直接進入來老年代。
優化收集方法的思路
分代收集算法
原理:根據對象存活的週期的不一樣將內存劃分爲幾塊,而後再選擇合適的收集算法。
通常是把java堆分紅新生代和老年代,這樣就能夠根據各個年待的特色採用最適合的收集算法。在新生代中,每次垃圾收集都會有大量的對象死去,只有少許存活,因此選用複製算法。老年代由於對象存活率高,沒有額外空間對他進行分配擔保,因此通常採用標記整理或者標記清除算法進行回收。
和稀泥解決方案,平時多數時間採用標記清除算法,暫時容忍內存碎片存在,直到內存空間碎片化程度達到影響對象分配時,採用標記整理算法收集一次。
3.4 HotSpot 的算法實現細節
3.2 3.3 介紹了常見的對象存活斷定算法和垃圾收集算法
3.4.1 根節點枚舉,3.4.2安全點,3.4.3安全區域(找到全部的GC Roots 必須暫停全部的用戶線程)
3.4.4記憶集(Remembered Set)與卡表
爲了解決對象跨代引用所帶來的問題,垃圾收集器在新生代中創建了名爲記憶集的數據結構
記憶集記錄了從非收集區域指向手機區域的指針集合的抽象數據結構
3.4.5寫屏障
3.4.6併發的可達性分析
固定做爲GC Roots的節點主要在全局性引用(例如常量或類靜態屬性),與執行上下文(例如棧楨中的本地變量表)中
根節點枚舉必須在一個能保障一致性的快照中得以進行
一致性:枚舉期間執行子系統看起來像被凍結在某個時間點上,不會出現分析過程當中,根節點集合的對象引用關係還在不斷變化的狀況,如果不能這點,就不能保證分析結果的準確性。
HotSpot沒有爲每一個指令都生成OopMap,前面提到的在「特定的位置」記錄下這些信息,這些位置稱爲安全點。
強制用戶程序必須執行到安全點纔可以暫停。就像高速開車只能在服務區停車休息同樣。
搶先式中斷
垃圾收集時,首先把全部用戶線程所有中斷,若是用戶線程沒有在安全點,就恢復該線程,直到到達安全點。
幾乎再也不使用該方式。
主動式中斷
a. 垃圾收集須要中斷線程時,僅簡單地設置一個標誌位,各線程不停地主動輪詢該標誌,一旦發現中斷標誌位爲真,就在最近的安全點停下
b. 輪詢標誌是和安全點重合的
c. hotSpot使用內存保護陷阱的方式把輪詢操做精簡到只有一條彙編指令。
當程序「不執行」時,如用戶線程在Sleep或Blocked,線程沒法響應中斷到安全點掛起本身,所以須要藉助安全區域。
安全區域就是可以確保在某段代碼中,引用關係不會發生變化,所以在這個區域任意位置進行GC都是安全的。(可理解爲被擴展拉伸的安全點)
2、在海量的對象中如何快速枚舉根節點?
咱們都知道在枚舉根節點或者GC的全過程是須要執行線程暫時停頓下來的,而考慮到效率問題,咱們又但願這個停頓越短暫越好。因此,目前主流的Java虛擬機都採用的是準確式GC(相對應的爲保守式GC),當執行系統停頓下來後,咱們不須要一個不漏地檢查完全部執行上下文和全局的引用位置。在HotSpot的實現中,是使用一組稱爲OOPMap的數據結構來達到這個目的的,首先在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描的時候,就能夠根據OOPMap上記錄的信息準肯定位到哪一個區域中有對象的引用,這樣大大減小了經過逐個遍從來找出對象引用的時間消耗。
爲了解決跨代引用問題,在新生代引入的記錄集(Remember Set)的數據結構(記錄從非收集區到收集區的指針集合),避免把整個老年代加入GCRoots掃描範圍。
垃圾收集場景中,收集器只需經過記憶集判斷出某一塊非收集區域是否存在指向收集區域的指針便可,無需瞭解跨代引用指針的所有細節。
能夠採用不一樣的記錄粒度,以節省記憶集的存儲和維護成本,如:
第三種卡精度是使用一種叫作「卡表」的方式實現記憶集,也是目前最經常使用的一種方式
記憶集是一種抽象概念,卡表是它的實現方式。它記錄了記憶集的記錄精度、與堆內存的映射關係等。
卡表是使用一個字節數組實現:CARD_TABLE[this addredd >>9]=0
,每一個元素對應着其標識的內存區域一塊特定大小的內存塊,稱爲「卡頁」。
hotSpot使用的卡頁是2^9大小,即512字節
一個卡頁中可包含多個對象,只要有一個對象的字段存在跨代指針,其對應的卡表的元素標識就變成1,表示該元素變髒,不然爲0.
GC時,只要篩選卡表中變髒的元素加入GCRoots。
卡表變髒上面已經說了,可是須要知道如何讓卡表變髒,即發生引用字段賦值時,如何更新卡表對應的標識爲1.
hotSpot使用寫屏障維護卡表狀態。
可看作在虛擬機層面對「引用類型字段賦值」動做的AOP切面,在賦值時產生一個環形通知。賦值先後都屬於寫屏障,賦值前稱爲「寫前屏障(Pre-Write Barrier)」,賦值後稱爲「寫後屏障(Post-Write Barrier)」。
虛擬機會爲全部賦值操做生成相應的指令,一旦收集器在寫屏障中增長了更新卡表操做,不管更新的是否是老年代對新生代的引用,每次只要對引用進行更新,就會產生額外的開銷。
僞共享問題
併發場景下,當多個互相獨立的變量被讀取到一個緩存行時,會影響性能。
僞共享參考:https://blog.csdn.net/weixin_43696529/article/details/104884373
解決:
加條件
只有卡表元素未被標記時纔將其標記爲變髒。
-XX:+UseCondCardMark
參數設定是否開啓卡表更新時的條件判斷開啓條件天然會增長一個判斷開銷,但可以避免僞共享問題。根據實際來。
可達性分析工做必需要在能保障一致性的快照中進行,所以必須中止用戶進程。
若是用戶進程被中止,那不會產生任何問題。
但若是用戶進程和GC進程併發進行,就會出現兩種後果:
使用三色標記來解釋上述問題:
白色: 對象未被收集器訪問(未掃描)
黑色:對象已被收集器訪問,且該對象全部引用已掃描過。(安全存活)(掃描完畢)
灰色:對象被訪問過,但對應至少還有一個引用沒有掃描過。(正在掃描)
但若是用戶線程在併發標記進行時修改了引用關係,以下狀況會出現存活對象消亡的現象:
如上圖:
同理,當標記到該圖的B時。取消 了B到C的引用,添加了A到D的引用,但由於A已經標記過,所以D不會再被掃描,C也不會被掃描,這樣D和C也因用戶線程的修改「意外死亡」,這就是「對象消失「的問題。
緣由1和2分別對應兩個解決方案:
增量更新(CMS用到)
記錄下新插入的引用,併發掃描完畢後,從新以記錄下的引用關係的黑色對象爲根掃描。
即黑色一旦插入了新的到白色的引用,就變成了灰色。
原始快照(G1和Shenandoah)
灰色對象要刪除指向白色對象的引用時,將該引用記錄下來,掃描完畢後,再從被記錄下的引用的灰色對象開始從新掃描。
HotSpot主要採用直接指針進行對象訪問。