GC垃圾回收機制

 

GC垃圾回收機制: 淺析與理解

對垃圾回收進行分析前,咱們先來了解一些基本概念java

基本概念

  • 內存管理:內存管理對於編程語言相當重要。彙編容許你操做全部東西,或者說要求你必須全權處理全部細節更合適。C 語言中雖然標準庫函數提供一些內存管理支持,可是對於以前調用 malloc 申請的內存,仍是依賴於你親自 free 掉。從C++、Python、Swift 和 Java 開始,纔在不一樣程度上支持內存管理。
  • 內存壓縮:對內存碎片進行壓縮。(和win10的那個「內存壓縮」不太同樣啦)
  • win10內存壓縮:物理內存已經見底,將一部分不常使用的內存數據打包壓縮起來,等到有程序須要訪問那些數據的時候,再解壓縮出來。
  • 引用與指針:git

    1. 引用被建立的同時必須被初始化(指針則能夠在任什麼時候候被初始化)。
    2. 不能有NULL 引用,引用必須與合法的存儲單元關聯(指針則能夠是NULL)。
    3. 一旦引用被初始化,就不能改變引用的關係(指針則能夠隨時改變所指的對象)。
    4. 引用只是某塊內存的別名。
    5. 實際上「引用」能夠作的任何事情「指針」也都可以作,爲何還要「引用」 這東西?
答案是「用適當的工具作恰如其分的工做」。好比說,某人須要一份證實,原本在文件上蓋                上公章的印子就好了,若是把取公章的鑰匙交給他,那麼他就得到了不應有的權利。(什麼狀況下,就用什麼對策)
6. 爲何還要說「只有指針,沒有引用是一個重要改變?」? 答案是雖然引用在某些狀況下好用,但他也會致使致命錯誤。以下: ``` char *pc = 0; // 設置指針爲空值 char& rc = *pc; // 讓引用指向空值 ``` 這是很是有害的,毫無疑問。結果將是不肯定的(編譯器能產生一些輸出,致使任何事情都有可能發生),應該躲開寫出這樣代碼的人除非他們贊成改正錯誤。若是你擔憂這樣的代碼會出如今你的軟件裏,那麼你最好徹底避免使用引用,要否則就去讓更優秀的程序員去作。 7. 最後上附圖,幫助理解

clipboard.png

  • 堆(heap)和棧(stack)程序員

    1. 日常說的「堆棧」實際上是棧。
    2. 棧,就是那些由編譯器在須要的時候分配,在不須要的時候自動清除的變量的存儲區。裏面的變量一般是局部變量、函數參數等。
    3. 堆,就是那些由new分配的內存塊,他們的釋放編譯器不去管,由咱們的應用程序去控 制,通常一個new就要對應一個delete。若是程序員沒有釋放掉,那麼在程序結束後,操做系統會自動回收。
  • 程序的棧結構github

    1. 程序的地址空間佈局: 程序運行靠四個東西:代碼、棧、堆、數據段。代碼段主要存放的就是可執行文件(一般可執行文件內,含有以二進制編碼的微處理器指令,也所以可執行文件有時稱爲二進制文件)中的代碼;數據段存放的就是程序中全局變量和靜態變量;堆中是程序的動態內存區域,當程序使用malloc或new獲得的內存是來自堆的;棧中維護的是函數調用的上下文,離開了棧就不可能實現函數的調用。
    2. 棧幀: 也叫活動記錄,保存的是一個函數調用所須要維護的全部信息。以下:
      1.函數的返回地址和參數
      2.臨時變量:包括函數的非靜態局部變量以及編譯器自動生成的其它臨時變量
      3.保存的上下文:包括在函數調用先後須要保存不變的寄存器值
    3. 就是它,先上圖

clipboard.png]算法

1.返回地址:一個main函數中斷執行的執行點. 2.ebp:指向函數活動記錄的一個固定位置,ebp又被稱爲幀指針.固定位置是,這樣在函數返回的時候,ebp就能夠經過這個恢復到調用前的值。 3.esp始終指向棧頂,所以隨着函數的執行,它老是變化的。 4.入棧順序:先壓這次調用函數參數入棧,接着是main函數返回地址,而後是ebp等寄存器。
  1. Link:C程序的函數棧做用機理(這個講得好,有實例,因此再也不熬述)

這裏咱們對比了解不一樣的 「找到須要標記的對象」的方法編程

可回收對象的斷定

  • 引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時, 計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。以下圖所示:
clipboard.pngsegmentfault

優勢:引用計數收集器能夠很快地執行,交織在程序的運行之中。這個特性對於程序不能被長時間打斷的實時環境頗有利。
缺點:很難處理循環引用,好比圖中相互引用的兩個對象則沒法釋放。
應用:Python 和 Swift 採用引用計數方案。 
  • 可達性分析算法(根搜索算法)

從GC Roots(每種具體實現對GC Roots有不一樣的定義)做爲起點,向下搜索它們引用的對象,能夠生成一棵引用樹,樹的節點視爲可達對象,反之視爲不可達。以下圖所示:
clipboard.png併發

  • 接下來補充幾個概念幫助理解(以java爲例):
  1. GC Roots對象:
    虛擬機棧(幀棧中的本地變量表)中引用的對象。
    方法區中靜態屬性引用的對象。
    方法區中常量引用的對象。
    本地方法棧中JNI引用的對象。

    本地方法棧則爲虛擬機所使用的Native方法服務。
    Native方法是指本地方法,當在方法中調用一些不是由java語言寫的代碼或者在方法中用java語言直接操縱計算機硬件。
    JNI:Java Native Interface縮寫,容許Java代碼和其餘語言寫的代碼進行交互。
    上述如圖,關於root區域的詳細解釋參考這裏
    clipboard.png編程語言


這裏咱們介紹幾種不一樣的 「標記對象」的方法函數

可回收對象的標記

  • 保守法

將全部堆上對齊的字都認爲是指針,那麼有些數據就會被誤認爲是指針。因而某些實際是數字的假指針,會背誤認爲指向活躍對象,致使內存泄露(假指針指向的對象多是死對象,但依舊有指針指向——這個假指針指向它)同時咱們不能移動任何內存區域。

  • 編譯器提示法

若是是靜態語言,編譯器可以告訴咱們每一個類當中指針的具體位置,而一旦咱們知道對象時哪一個類實例化獲得的,就能知道對象中全部指針。這是JVM實現垃圾回收的方式,但這種方式並不適合JS這樣的動態語言

  • 標記指針法

標記指針法:這種方法須要在每一個字末位預留一位來標記這個字段是指針仍是數據。這種方法須要編譯器支持,但實現簡單,並且性能不錯。V8採用的是這種方式。

  • 位圖標記(Go語言爲例)
  1. 非侵入式標記位定義
    既然垃圾回收算法要求給對象加上垃圾回收的標記,顯然是須要有標記位的。通常的作法
    會將對象結構體中加上一個標記域,一些優化的作法會利用對象指針的低位進行標記,這
    都只是些奇技淫巧罷了。Go沒有這麼作,它的對象和C的結構體對象徹底一致,使用的是
    非侵入式的標記位。
  2. 具體實現
    堆區域對應了一個標記位圖區域,堆中每一個字(不是byte,而是word)都會在標記位區域
    中有對應的標記位。每一個機器字(32位或64位)會對應4位的標記位。所以,64位系統中
    至關於每一個標記位圖的字節對應16個堆中的字節。

    雖然是一個堆字節對應4位標記位,但標記位圖區域的內存佈局並非按4位一組,而是
    16個堆字節爲一組,將它們的標記位信息打包存儲的。每組64位的標記位圖從上到下依
    次包括:

    16位的 特殊位 標記位 16位的 垃圾回收 標記位 16位的 無指針/塊邊界 的標記位 16位的 已分配 標記位

    這樣設計使得對一個類型的相應的位進行遍歷很容易。

    前面提到堆區域和堆地址的標記位圖區域是分開存儲的,其實它們是以 
    mheap.arena_start地址爲邊界,向上是實際使用的堆地址空間,向下則是標記位圖區
    域。以64位系統爲例,計算堆中某個地址的標記位的公式以下:

    偏移 = 地址 - mheap.arena_start
    標記位地址 = mheap.arena_start - 偏移/16 - 1 移位 = 偏移 % 16 標記位 = *標記位地址 >> 移位

    而後就能夠經過 (標記位 & 垃圾回收標記位),(標記位 & 分配位),等來測試相應的位。

    (也就是說,原本64位是一個字,須要4位標記位。可是,爲了與字長相對,16個標記位
    放一塊兒(恰好一個字長)一塊兒表示16個字。而且每類標記位都放在一塊兒
    AA..AABB...BB)

  • 接下來補充幾個概念幫助理解:
  1. 爲何要判斷哪些是數據,哪些是指針?
    假設堆中有一個long的變量,它的值是8860225560。可是咱們不知道它的類型是
    long,因此在進行垃圾回收時會把個看成指針處理,這個指針引用到了0x2101c5018位
    置。假設0x2101c5018碰巧有某個對象,那麼這個對象就沒法被釋放了,即便實際上已
    經沒任何地方使用它。

    因爲沒有類型信息,咱們並不知道這個結構體成員不包含指針,所以咱們只能對結構體
    的每一個字節遞歸地標記下去,這顯然會浪費不少時間。
    (能不能清除 變成了機率事件)。

  2. 垃圾收集器(CMS收集器爲例)
    幾個階段:

    初始標記
    併發標記
    最終標記
    篩選回收

    初始標記僅僅是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記就是進行
    GC Roots Trancing的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼
    續運行而致使標記產生變更那一部分對象的標記記錄,這個階段的停頓時間比初始標記稍
    長一些,但遠比並發標記時間短。

  3. stop the world
    由於垃圾回收的時候,須要整個的引用狀態保持不變,不然斷定是垃圾,等我稍後回
    收的時候它又被引用了,這就全亂套了。因此,GC的時候,其餘全部的程序執行處於暫停
    狀態,卡住了。
    這個概念提早引入,在這裏進行對比,效果會更好些。與標記階段對比,stop the world發生在回收階段。

這裏咱們介紹幾種不一樣的垃圾回收算法

垃圾回收算法

  • 標記清除算法 (Mark-Sweep)

標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出全部須要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。

優勢是簡單,容易實現。
缺點是容易產生內存碎片,碎片太多可能會致使後續過程當中須要爲大對象分配空間時沒法找到足夠的空間而提早觸發新的一次垃圾收集動做。(由於沒有對不一樣生命週期的對象採用不一樣算法,因此碎片多,內存容易滿,gc頻率高,耗時,看了後面的方法就明白了)

clipboard.png  =10x10

  • 分代回收算法

根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。不一樣區域採用不一樣算法(複製算法,標記整理算法),這就是分代回收算法。

通常狀況下將堆區劃分爲老年代(Old Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集時只有少許對象須要被回收,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。

1.新生代回收

新生代使用Scavenge算法進行回收。在Scavenge算法的實現中,主要採用了Cheney算法。

Cheney算法是一種採用複製的方式實現的垃圾回收算法。 它將內存一分爲二,每一部分空間稱爲semispace。在這兩個semispace中,一個處於使用狀態,另外一個處於閒置狀態。 簡而言之,就是經過將存活對象在兩個semispace空間之間進行復制。 複製過程採用的是BFS(廣度優先遍歷)的思想,從根對象出發,廣度優先遍歷全部能到達的對象 優勢:時間效率上表現優異(犧牲空間換取時間) 缺點:只能使用堆內存的一半

新生代的空間劃分比例爲何是比例爲8:1:1(不是按照上面算法中說的1:1)?

新建立的對象都是放在Eden空間,這是很頻繁的,尤爲是大量的局部變量產生的臨時對
象,這些對象絕大部分都應該立刻被回收,能存活下來被轉移到survivor空間的每每不
多。因此,設置較大的Eden空間和較小的Survivor空間是合理的,大大提升了內存的使
用率,緩解了Copying算法的缺點。

8:1:1就挺好的,固然這個比例是能夠調整的,包括上面的新生代和老年代的1:2的 比例也是能夠調整的。

Eden空間和兩塊Survivor空間的工做流程是怎樣的?
clipboard.png

具體的執行過程是怎樣的?

假設有相似以下的引用狀況:
+----- A對象 | 根對象----+----- B對象 ------ E對象 | +----- C對象 ----+---- F對象 | +---- G對象 ----- H對象 D對象 
在執行Scavenge以前,From區長這幅模樣:
``` +---+---+---+---+---+---+---+---+--------+ | A | B | C | D | E | F | G | H | | +---+---+---+---+---+---+---+---+--------+ ``` 那麼首先將根對象能到達的ABC對象複製到To區,因而乎To區就變成了這個樣子: ```  allocationPtr  ↓ +---+---+---+----------------------------+ | A | B | C | | +---+---+---+----------------------------+  ↑ scanPtr ``` 接下來進入循環,掃描scanPtr所指的A對象,發現其沒有指針,因而乎scanPtr移動,變成以下這樣 ```  allocationPtr  ↓ +---+---+---+----------------------------+ | A | B | C | | +---+---+---+----------------------------+  scanPtr ``` 接下來掃描B對象,發現其有指向E對象的指針,且E對象在From區,那麼咱們須要將E對象複製到allocationPtr所指的地方並移動allocationPtr指針: ```  allocationPtr  ↓ +---+---+---+---+------------------------+ | A | B | C | E | | +---+---+---+---+------------------------+  scanPtr ``` 中間過程省略,具體參考[新生代的垃圾回收具體的執行過程][3] From區和To區在複製完成後的結果: ``` //From區 +---+---+---+---+---+---+---+---+--------+ | A | B | C | D | E | F | G | H | | +---+---+---+---+---+---+---+---+--------+ //To區 +---+---+---+---+---+---+---+------------+ | A | B | C | E | F | G | H | | +---+---+---+---+---+---+---+------------+ ```

最終當scanPtr和allocationPtr重合,說明覆制結束。
注意:若是指向老生代咱們就沒必要考慮它了。(經過寫屏障)

對象什麼時候晉升?

1.當一個對象通過屢次新生代的清理依舊倖存。 2.若是To空間已經被使用了超過25%(後面還要進來許多新對象,不敢佔用太多) 3.大對象 (其實這部分,包括次數,比例等,是視狀況設置的。) 

2.老生代回收

Mark-Sweep(標記清除)

標記清除分爲標記和清除兩個階段。
主要是標記清除只清除死亡對象,而死亡對象在老生代中佔用的比例很小,因此效率較高。

Mark-Compact(標記整理)

標記整理正是爲了解決標記清除所帶來的內存碎片的問題。

大致過程就是 雙端隊列標記黑(鄰接對象已經所有處理),白(待釋放垃圾),灰(鄰
接對象還沒有所有處理)三種對象.
 
標記算法的核心就是深度優先搜索.
  • 補充概念方便理解

1.觸發GC(什麼時候發生垃圾回收?)

通常都是內存滿了就回收,下面列舉幾個常見緣由:
GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。 GC_CONCURRENT: 當咱們應用程序的堆內存達到必定量,或者能夠理解爲快要滿的時候,系統會自動觸發GC操做來釋放內存。 GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。 GC_BEFORE_OOM: 表示是在準備拋OOM異常以前進行的最後努力而觸發的GC。 

2.寫屏障(一個老年代的對象須要引用年輕代的對象,該怎麼辦?)

若是新生代中的一個對象只有一個指向它的指針,而這個指針在老生代中,咱們如何判斷
這個新生代的對象是否存活?爲了解決這個問題,須要創建一個列表用來記錄全部老生代
對象指向新生代對象的狀況。每當有老生代對象指向新生代對象的時候,咱們就記錄下
來。
當垃圾回收發生在年輕代時,只需對這張表進行搜索以肯定是否須要進行垃圾回收,而不
是檢查老年代中的全部對象引用。

3.深度、廣度優先搜索(爲何新生代用廣度搜索,老生代用深度搜索)

深度優先DFS通常採用遞歸方式實現,處理tracing的時候,可能會致使棧空間溢出,因此通常採用廣度優先來實現tracing(遞歸狀況下容易爆棧)。
廣度優先的拷貝順序使得GC後對象的空間局部性(memory locality)變差(相關變量散開了)。 廣度優先搜索法通常無回溯操做,即入棧和出棧的操做,因此運行速度比深度優先搜索算法法要快些。 深度優先搜索法佔內存少但速度較慢,廣度優先搜索算法佔內存多但速度較快。 結合深搜和廣搜的實現,以及新生代移動數量小,老生代數量大的狀況,咱們能夠獲得瞭解答。源地址:https://segmentfault.com/a/1190000005654932
相關文章
相關標籤/搜索