3.垃圾收集器與內存分配

年輕代、老年代、永久代

四種引用類型

3.1java

哪些須要回收算法

何時回收數組

怎麼回收緩存

垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾佔用的空間,防止內存泄露。有效的使用可使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。安全

3.2對象已死數據結構

Java堆中存放着幾乎Java世界中的全部對象,垃圾收集器在回收對象以前須要知道那些對象「還活着」,那些對象「已經死去」(即不可能再被任何途徑使用的對象)。併發

3.2.1引用計數算法性能

給每一個對象添加一個計數器,當有地方引用該對象時計數器加1,當引用失效時計數器減1。用對象計數器是否爲0來判斷對象是否可被回收。缺點:沒法解決循環引用的問題。優化

3.2.2可達性分析算法this

經過 GC ROOT的對象做爲搜索起始點,經過引用向下搜索,所走過的路徑稱爲引用鏈。經過對象是否有到達引用鏈的路徑來判斷對象是否可被回收
可做爲 GC ROOT的對象:
虛擬機棧(棧幀中的本地變量表)中引用的對象,好比各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
方法區中類靜態屬性引用的對象,好比Java類的引用類型靜態變量
方法區中常量引用的對象,字符串常量池中的引用
本地方法棧中 JNI(一般所說的Native方法)引用的對象
Java虛擬機內部的引用,如基本數據所對應的Class對象,一些常駐的異常對象(好比NullpointeException,OutOfMemoryError)deng,還有系統類加載器。
全部被同步鎖(synchronized關鍵字)持有的對象。
反映Java虛擬機內部的狀況JMXBean。JVMTI中註冊的回調、本地代碼緩存等。
分代收集和局部回收:若是隻針對Java堆某一塊區域發起垃圾收集時,必須考慮內存區域是虛擬機本身的實現細節,更不是孤立封閉的,因此在某個區域裏的對象徹底有可能被位於堆中其餘區域的對象所引用,這時候就須要將這些關聯區域的對象也一併加入GC Roots集合中去,才能保證可達性分析的正確性。
 
3.2.3再談引用(不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象的引用鏈是否可達,斷定對象是否存活都與「引用」有關。)
  • 在JDK1.2之前,Java中的引用的定義很傳統:若是reference類塑的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義很純粹,可是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些「食之無味,棄之惋惜」的對象就顯得無能爲力。咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之屮;若是內存空間在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。
  • 在JDK 1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱
1.強引用:就是指在程序代碼之中廣泛存在的,相似「Objectobj = new Object()」這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
2.軟引用:是用來描述一些還有用但並不是必需的對象。在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。
3.弱引用:也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
4.虛引用:最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。
 
3.2.4生存仍是死亡

即便在可達性分析算法中不可達的對象,也不是「非死不可」的。

對象在被標記爲不可達以後,若是對象覆蓋了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.斷定一個類是「無用的」較爲苛刻,需知足三個條件,知足如下三個條件無用類才能夠被回收(僅僅是能夠,是否必然回收虛擬機有其餘參數控制):

  • 該類全部的實例都已被回收,也就是Java堆中不存在該類的任何實例。
  • 加載該類的classloader已被回收。
  • 該類對應的java.long.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該方法。

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併發的可達性分析

 

3.1 根節點枚舉

  1. 迄今爲止,全部收集器在根節點枚舉這一步驟都必須暫停用戶線程,會面臨Stop The World的困擾
  2. 當前主流的虛擬機都使用準確是垃圾收集,所以虛擬機有辦法直接獲得哪些地方存在對象引用。HotSpot使用一組OopMap的數據結構來達到這個目的
  3. 一旦類加載動做完成,HotSpot 就會把對象內什麼偏移量上是什麼類型的數據計算出來(在即時編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用),這樣收集器在掃描時就能直接得知這些信息,沒必要從方法區等 GC Roots 開始查找,提升查找效率。
  4.  固定做爲GC Roots的節點主要在全局性引用(例如常量或類靜態屬性),與執行上下文(例如棧楨中的本地變量表)中

      根節點枚舉必須在一個能保障一致性的快照中得以進行

      一致性:枚舉期間執行子系統看起來像被凍結在某個時間點上,不會出現分析過程當中,根節點集合的對象引用關係還在不斷變化的狀況,如果不能這點,就不能保證分析結果的準確性。

3.2 安全點

HotSpot沒有爲每一個指令都生成OopMap,前面提到的在「特定的位置」記錄下這些信息,這些位置稱爲安全點。

強制用戶程序必須執行到安全點纔可以暫停。就像高速開車只能在服務區停車休息同樣。

  • 安全點太少,會讓收集器等候太久
  • 安全點太多,會過度增大運行時的內存負荷
  • 安全點的選取以是否具備讓程序長時間執行的特徵
  • 「長時間執行」最明顯的特徵是:指令列的複用,如方法調用、循環跳轉、異常跳轉等,具備這種功能的指令纔會產生安全點。

如何在垃圾收集時讓全部線程跑到最近的安全點停頓下來

  1. 搶先式中斷

    垃圾收集時,首先把全部用戶線程所有中斷,若是用戶線程沒有在安全點,就恢復該線程,直到到達安全點。

    幾乎再也不使用該方式。

  2. 主動式中斷

    a. 垃圾收集須要中斷線程時,僅簡單地設置一個標誌位,各線程不停地主動輪詢該標誌,一旦發現中斷標誌位爲真,就在最近的安全點停下

    b. 輪詢標誌是和安全點重合的

    c. hotSpot使用內存保護陷阱的方式把輪詢操做精簡到只有一條彙編指令。

3.3 安全區域

當程序「不執行」時,如用戶線程在Sleep或Blocked,線程沒法響應中斷到安全點掛起本身,所以須要藉助安全區域。

安全區域就是可以確保在某段代碼中,引用關係不會發生變化,所以在這個區域任意位置進行GC都是安全的。(可理解爲被擴展拉伸的安全點)

原理

在這裏插入圖片描述

2、在海量的對象中如何快速枚舉根節點?
     咱們都知道在枚舉根節點或者GC的全過程是須要執行線程暫時停頓下來的,而考慮到效率問題,咱們又但願這個停頓越短暫越好。因此,目前主流的Java虛擬機都採用的是準確式GC(相對應的爲保守式GC),當執行系統停頓下來後,咱們不須要一個不漏地檢查完全部執行上下文和全局的引用位置。在HotSpot的實現中,是使用一組稱爲OOPMap的數據結構來達到這個目的的,首先在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描的時候,就能夠根據OOPMap上記錄的信息準肯定位到哪一個區域中有對象的引用,這樣大大減小了經過逐個遍從來找出對象引用的時間消耗。

3.4 記憶集與卡表

爲了解決跨代引用問題,在新生代引入的記錄集(Remember Set)的數據結構(記錄從非收集區到收集區的指針集合),避免把整個老年代加入GCRoots掃描範圍。

垃圾收集場景中,收集器只需經過記憶集判斷出某一塊非收集區域是否存在指向收集區域的指針便可,無需瞭解跨代引用指針的所有細節。

3.4.1 記憶集的實現

能夠採用不一樣的記錄粒度,以節省記憶集的存儲和維護成本,如:

  • 字長精度:每一個記錄精確到一個機器字長(處理器的尋址位數,如常見的 32 位或 64 位),該字包含跨代指針
  • 對象精度:每一個記錄精確到一個對象,該對象中有字段包含跨代指針
  • 卡精度:每一個記錄精確到一塊內存區域,該區域中有對象包含跨代指針
卡表

第三種卡精度是使用一種叫作「卡表」的方式實現記憶集,也是目前最經常使用的一種方式

記憶集是一種抽象概念,卡表是它的實現方式。它記錄了記憶集的記錄精度、與堆內存的映射關係等。

卡表是使用一個字節數組實現:CARD_TABLE[this addredd >>9]=0,每一個元素對應着其標識的內存區域一塊特定大小的內存塊,稱爲「卡頁」。

hotSpot使用的卡頁是2^9大小,即512字節

在這裏插入圖片描述

一個卡頁中可包含多個對象,只要有一個對象的字段存在跨代指針,其對應的卡表的元素標識就變成1,表示該元素變髒,不然爲0.

GC時,只要篩選卡表中變髒的元素加入GCRoots。

3.5 寫屏障

3.5.1 卡表的維護

卡表變髒上面已經說了,可是須要知道如何讓卡表變髒,即發生引用字段賦值時,如何更新卡表對應的標識爲1.

hotSpot使用寫屏障維護卡表狀態。

可看作在虛擬機層面對「引用類型字段賦值」動做的AOP切面,在賦值時產生一個環形通知。賦值先後都屬於寫屏障,賦值前稱爲「寫前屏障(Pre-Write Barrier)」,賦值後稱爲「寫後屏障(Post-Write Barrier)」。

3.5.2 寫屏障的問題

  1. 虛擬機會爲全部賦值操做生成相應的指令,一旦收集器在寫屏障中增長了更新卡表操做,不管更新的是否是老年代對新生代的引用,每次只要對引用進行更新,就會產生額外的開銷。

  2. 僞共享問題

    併發場景下,當多個互相獨立的變量被讀取到一個緩存行時,會影響性能。
    僞共享參考:https://blog.csdn.net/weixin_43696529/article/details/104884373

    解決:

    1. 加條件

      只有卡表元素未被標記時纔將其標記爲變髒。

      1. JDK7之後,使用 -XX:+UseCondCardMark參數設定是否開啓卡表更新時的條件判斷

    開啓條件天然會增長一個判斷開銷,但可以避免僞共享問題。根據實際來。

3.6 併發的可達性分析

可達性分析工做必需要在能保障一致性的快照中進行,所以必須中止用戶進程。

若是用戶進程被中止,那不會產生任何問題。

但若是用戶進程和GC進程併發進行,就會出現兩種後果:

  1. 錯誤地標記已經消亡的對象(用戶從新創建引用關係)
  2. 將存活的對象標記爲消亡

3.6.1 三色標記

使用三色標記來解釋上述問題:

白色: 對象未被收集器訪問(未掃描)

黑色:對象已被收集器訪問,且該對象全部引用已掃描過。(安全存活)(掃描完畢)

灰色:對象被訪問過,但對應至少還有一個引用沒有掃描過。(正在掃描)

在這裏插入圖片描述
在這裏插入圖片描述

但若是用戶線程在併發標記進行時修改了引用關係,以下狀況會出現存活對象消亡的現象:

在這裏插入圖片描述

​ 如上圖:

  1. 本來引用關係爲:A->B,B->C
  2. 掃描到B時,用戶線程取消了B到C的引用,反而添加了一條從已掃描過的對象A到對象C的引用
  3. 2的狀況就會形成對象C不被掃描到,而C本應該是存活的,卻在這個狀況下意外地被標記爲」死亡「

在這裏插入圖片描述
同理,當標記到該圖的B時。取消 了B到C的引用,添加了A到D的引用,但由於A已經標記過,所以D不會再被掃描,C也不會被掃描,這樣D和C也因用戶線程的修改「意外死亡」,這就是「對象消失「的問題。

對象消失的緣由

  1. 添加了一條或多條從黑色到白色的新引用
  2. 刪除了所有從灰色到白色的舊引用(直接or間接)

對象消失的解決

緣由1和2分別對應兩個解決方案:

  1. 增量更新(CMS用到)

    記錄下新插入的引用,併發掃描完畢後,從新以記錄下的引用關係的黑色對象爲根掃描。

    即黑色一旦插入了新的到白色的引用,就變成了灰色。

  2. 原始快照(G1和Shenandoah)

    灰色對象要刪除指向白色對象的引用時,將該引用記錄下來,掃描完畢後,再從被記錄下的引用的灰色對象開始從新掃描。

HotSpot主要採用直接指針進行對象訪問。

相關文章
相關標籤/搜索