垃圾回收(GC)那些事兒

前言

垃圾回收是什麼,大家是否是想到垃圾車來收垃圾了?耳邊響起東方紅?前端

是否是還有人想到想到的是廢品回收:收電飯煲、高壓窩、煤氣竈~~~node

其實今天咱們要講的是內存中·垃圾回收啦~面試

第一次瞭解垃圾回收是在一個公衆號看到的,當時講了一下標記清除法和引用計數法,可是當時存在不少疑惑,好比可達不可達究竟是什麼?當時也沒太在乎。今天從新瞭解,用本身的話總結分享出來。但願能給您一些啓發和思考。算法

這篇文章包含如下知識點:數組

  • 什麼是垃圾回收
  • 如何判斷是否爲垃圾
    • 可達性
  • 垃圾回收算法有哪些,各自的特色
  • 內存泄漏

在文章開始前要知道一個很重要的知識,JS的內存生命週期。今天要講的就是那些關於內存釋放時的故事。瀏覽器

  • 1.爲變量分配內存
  • 2.使用分配的內存
  • 3.不須要的時候將內存釋放

1、什麼是垃圾回收

首先我認爲垃圾回收比較難理解的一個緣由是:它比較抽象,畢竟是瀏覽器內部的操做,是咱們肉眼不可見的。bash

能夠想象咱們平常生活中那些廢棄的沒用的東西就是「垃圾」,爲了佔位置就要將生活垃圾清理掉。js內存管理也是如此,只不過他的內存管理是自動執行的,咱們不可見的(可是在沒有垃圾回收機制的語言中就須要人爲管理內存,好比c語言)。數據結構

知識點1閉包

JavaScript 引擎中有一個後臺進程稱爲垃圾回收器,它監視全部對象,並刪除那些不可訪問的對象(垃圾)。dom

知識點2

咱們在建立一個字符串、數組等都看做對象(無論是基本類型仍是引用類型)都會爲這個對象開闢一個內存空間來保存這個變量。若是訪問不到這個對象的時候(沒用了)就是垃圾

那麼你可能會問, 不可訪問的對象是什麼呢?怎麼知道對象是否能夠訪問呢?下面就由我一一爲你們道來

2、如何判斷垃圾

如何判斷垃圾前面說過就是看這個對象可否被訪問,那如何知道對象可否被訪問?有一個專業的詞叫可達性。根據對象是否可達來判斷。

可達性

JavaScript 中內存管理的主要概念是可達性。

簡單地說,「可達性」 值就是那些以某種方式可訪問或可用的值,它們被保證存儲在內存中。先看一個例子吧

//定義一個user對象,引用name屬性
const user={
    name:"john"
}
複製代碼

這裏箭頭表示一個對象引用。全局變量user引用對象 {name:「John」} ,user 的 「name」 屬性存儲一個基本類型,所以它被繪製在對象中。

若是 user 的值被覆蓋,則引用丟失:

例1

//讓user的引用爲空
user=null
複製代碼

這個時候經過user就沒有辦法訪問到name這個屬性,更沒辦法獲得屬性值。

圖中的箭頭表示引用,第一個圖user引用name屬性,第二圖讓user指向空,箭頭消失,user沒法引用name屬性,js引擎將 {name:「John」}回收到垃圾桶處理掉,釋放了內存空間。

圈個重點👇

user能夠訪問到name屬性,那name是可達的;沒法訪問那麼name就是不可達的。

看了上面這個例子不知道對可達性是否有基礎的認識了呢?接着咱們繼續深刻可達性。

  • 有一組基本的固有可達值,因爲顯而易見的緣由沒法刪除, 例如:

    • 本地函數的局部變量和參數

    • 當前嵌套調用鏈上的其餘函數的變量和參數

    • 全局變量

    • 還有一些其餘的,內部的

上面這些值稱爲根。

  • 若是引用或引用鏈能夠從根訪問任何其餘值,則認爲該值是可訪問的。

如今咱們又多了一個概念那就是。接着來看一個例子。

例2

// user具備對象的引用
let user = {
  name: "John"
};

let admin = user;
複製代碼

咱們建立一個全局新對象admin,它和user同樣引用了同一個變量。此時name是可達的,若是咱們進行下面的操做,它仍是可達的嗎?

admin=null;
複製代碼

結果是name屬性仍是可達的,爲何呢?不是已經刪除了admin對name的引用嗎?

緣由是:雖然admin沒有辦法引用name,可是user仍是能夠引用name屬性的,所以能夠從根訪問到name屬性,所以他仍是可達的。

若是再讓user=null;那name纔會變成不可達。這個時候沒法從引用name屬性了。

上面的圖片都來自 這裏。咱們繼續來看看垃圾回收算法有哪些。

3、垃圾回收算法

這裏主要介紹兩種主要回收算法,若是想了解更多,好比標記壓縮,GC複製等能夠點擊這裏

引用計數法

引用計數法也很好理解,就是引用對引用的次數進行計數。若是引用了增長就加1,引用減小就減去1.當引用等於0將它清除。看一個例子

  • 例3
//假若有一個計數器count=0
let a ={
    name:'linglong',//count==1
}
let b=a;           //count==2
b=null;            //count==1
a=null;            //count==0,被清除
複製代碼

假若有一個引用計數的計數器count,依次進行上面四步操做,對於name的引用從0->1->2->1->0。最後被回收。

引用計數的問題

引用計數有一個致命的問題就是循環引用,若是兩個對象互相引用,儘管再也不使用可是會進入一個無限循環,垃圾回收器不會對他進行回收。看下面代碼

  • 例4
function cycle(){
    var o1={};
    var o2={};
    o1.a=o2;
    o2.a=o1;
}
cycle();
複製代碼

這個代碼中cycle函數執行完後不須要了,因此o1和o2的內存應該被釋放,可是他們互相引用致使內存不會被回收,如今通常不會使用這個方法,可是ie9以前仍然還在用。如今用的較多的是後面介紹的標記清除法。

標記清除算法

標記清除法分爲兩大步,先標記而後清除沒有被標記的。

  • 垃圾回收器獲取根並標記(記住)它們。
    • 而後它訪問並「標記」全部來自它們的引用。
    • 而後它訪問標記的對象並標記它們的引用。全部被訪問的對象都被記住,以便之後再也不訪問同一個對象兩次。
    • 以此類推,直到有未訪問的引用(能夠從根訪問)爲止。
  • 除標記的對象外,全部對象都被清除

例如,對象結構以下:

咱們能夠清楚地看到右邊有一個「不可到達的塊」。如今讓咱們看看**「標記並清除」**垃圾回收器如何處理它。

  • 第一步標記根

- 而後標記他們的引用

-以及子孫代的引用:

  • 如今進程中不能訪問的對象被認爲是不可訪問的,將被刪除:

標記清除算法數據結構

標記清除法利用到了堆、鏈表結構 標記階段:從根集合出發,將全部活動對象及其子對象打上標記 清除階段:遍歷堆,將非活動對象(未打上標記)的鏈接到空閒鏈表上

這就是垃圾收集的工做原理。JavaScript引擎應用了許多優化,使其運行得更快,而且不影響執行。

V8引擎一些優化:

分代回收

v8堆中對象對象分爲兩組:新生代和老生代

  • 新生代:大多數對象的建立被分配在這裏,這個區域很小,但垃圾回收很是頻繁,獨立於其它區存活期短,如臨時變量字符串等
  • 老生代
    • 老生代指針區:包含大部分可能含有指向其它對象指針的對象。大多數重新生代晉升(存活一段時間)的對象會被移動到這裏。
    • 老生代數據:區包含原始數據對象(沒有指針指向其它對象)。Strings、boxed numbers以及雙精度unboxed數組重新生代中晉升後被移到這裏。
增量回收

若是有不少對象,而且咱們試圖一次遍歷並標記整個對象集,那麼可能會花費一些時間,並在執行中會有必定的延遲。所以,引擎試圖將垃圾回收分解爲多個部分(18年提出)。而後,各個部分分別執行。這須要額外的標記來跟蹤變化,這樣有不少微小的延遲,而不是很大的延遲。

空閒時間收集

垃圾回收器只在 CPU 空閒時運行,以減小對執行的可能影響。

內存泄露

因爲內存泄露和內存沒有被釋放有關,因此這裏簡單介紹下何時會產生內存泄露吧!

知識點1

什麼是內存泄露,對於再也不用到的內存若是沒有及時釋放就叫內存泄露。

這和泄露有半毛錢關係???

我是這樣理解的,咱們把內存比做手內心的沙子,當沙子從手裏漏了出去,那麼可用的內存就愈來愈少了,這個過程就是內存泄露。

手裏握不住的沙,不如揚了它

可是內存泄露了仍是要管一下的啦,如何對它負責,請接着往下看

四種內存泄露

理解了內存泄露的概念後,咱們要知道如下幾種狀況會致使內存泄露

  • 意外的全局變量(嚴格模式解決)
  • 被遺忘的定時器和回調函數
  • 脫離dom的引用
  • 閉包(變量引用指向null解決)

第一個和第四個很好理解,若是有問題能夠評論區找linglong,十分歡迎。主要講講二、3兩個。

  • 二、被遺忘的定時器和回調函數

當不須要setInterval或者setTimeout時,定時器沒有被clear,定時器的回調函數以及內部依賴的變量都不能被回收,形成內存泄漏。

timeout = setTimeout(() => {
 var node = document.getElementById('node');
    if(node){
          fn();
    }
    }, 1000);
複製代碼

解決方法: 在定時器完成工做的時候,手動清除定時器。timeout=null

  • 三、脫離dom的引用
<body>
    <div id="fa">
        <button id="button"></button>
    </div>
    <script>
        let div=document.getElementById('fa');
        document.body.removeChild(div); // dom刪除了
        //div=null          //切斷div對div的引用
        console.log(div);
    </script>
</body>
複製代碼

結果:

<div id="fa">
        <button id="button"></button>
 </div>
複製代碼

咱們能夠看到結果中div並無被刪除,這是由於代碼中刪除的div是dom樹中div,let div=document.getElementById('fa');這句代碼中存在着div對div的引用。

solution:咱們要經過div=null將兩次引用都切掉。

其餘內存泄露狀況

除了上面四種,若是有其餘的內存泄露狀況歡迎指出,一塊兒學習,嘻嘻嘻

內存泄露的識別方法

內存泄露識別方法的內容來自LinDaiDai_霖呆呆小哥哥的建議,超級nice的做者😃😉

一、瀏覽器中識別(Chrome瀏覽器的控制檯Performance或Memory)

這裏展現performance裏面查看的方法。主要步驟以下:

  1. 在網頁上右鍵, 點擊「檢查」打開控制檯(Mac快捷鍵option+command+i);
  2. 選擇Performance面板(下圖的步驟1)
  3. 勾選Memory, 而後點擊左上角的小黑點Record開始錄製(下圖的步驟二、3)
  4. 點擊彈窗中的Stop結束錄製, 面板上就會顯示這段時間的內存佔用狀況。
    完成前面三步開始錄製就是下面這個樣子。
    第4部結束錄製後的樣子以下圖。若是內存使用狀況一直在作增量,就是內存泄露了。
    ps:打開控制檯中Memory面板也是能夠檢查的,在LinDaiDai_霖呆呆記錄一次定時器及閉包問題形成的內存泄漏一文中有詳細講解

二、命令行方法

命令行可使用 Node 提供的process.memoryUsage方法。玲瓏對node.JS不熟悉,在這以前不知道這個方法的。經查閱官方文檔後得知:

process 是一個全局變量,即 global全局對象下的一個的屬性它用於描述當前Node.js 進程狀態的對象。

memoryUsage是process下的一個方法,返回一個對象,描述了 Node 進程所用的內存情況,單位爲字節。

接下來一塊兒實踐一下吧

  1. 新建一個main.js文件,輸入如下代碼
const fun = () => {
    console.log(__filename);//文件所在位置的絕對路徑
    console.log('下面是內存使用信息:');
    console.log(process.memoryUsage());
}
fun();
複製代碼
  1. 在終端輸入node main.js回車能夠看到執行後結果,以下圖所示:

3. 結果說明

咱們看到process.memoryUsage返回一個對象,包含了 Node 進程的內存佔用信息。該對象包含四個字段,單位是字節,含義以下。

rss(resident set size):全部內存佔用,包括指令區和堆棧。
heapTotal:"堆"佔用的內存,包括用到的和沒用到的。
heapUsed:用到的堆的部分。
external: V8 引擎內部的 C++ 對象佔用的內存。
複製代碼

注意:判斷內存泄漏,以heapUsed字段爲準

最後給你們一個思考題:如何減小內存泄露?能夠評論區留言哦

總結

看完了給本身一個大大的贊吧,能夠問問本身:

  • 什麼是垃圾回收,什麼是可達性
  • 垃圾回收算法,哪兩種
  • 內存泄露常見的有哪些,如何解決

認真看完必定會有收穫滴,比心~

玲瓏以爲若是垃圾回收算法的話能夠聊不少. 從內存機制開始講起,什麼是垃圾回收,垃圾回收算法,v8引擎如何回收等,內存泄露,以及ES6中國的Weakset和WeakMap這兩個不計入垃圾回收機制的弱引用....

若是我有哪些理解不對的地方還請掘友們指正,若是誤導你們就尷尬啦

另外文章封面「除了money都是垃圾」是一句玩笑話啦,畢竟仍是有不少東東高於毛爺爺的,好比大家的star~和留言!

再次感謝LinDaiDai_霖呆呆小哥哥的鼓勵和幫助!

參考文章

[譯] 前端面試:談談 JS 垃圾回收機制

推薦閱讀

咱們下篇文章見...

相關文章
相關標籤/搜索