你還要我怎樣的JS系列(1)--內(nèi)存空間

1. 內(nèi)存空間

波比小金剛
本文地址

前言


一直以來對JS的理解和認(rèn)識(shí)總是零散雜亂房交。近期希望整理出一條主線來挪蹭,把JS的各路好漢串聯(lián)起來诵棵。

我相信很多人和我一樣舶胀,對JS這門動(dòng)態(tài)弱類型語言的學(xué)習(xí)常常是倒過來的,就是咱先用著网梢,然后再時(shí)不時(shí)的看些知識(shí)點(diǎn)補(bǔ)充震缭。

為了面試或者裝逼,常常從言語不可描述的角度去看待這門語言战虏,本身無可厚非拣宰。

怎奈我就是一俗人,希望用我粗暴淺顯的理解烦感,去重新認(rèn)識(shí)JavaScript巡社,擁抱JavaScript(此處換成小澤老師、蒼井老師......)手趣。

進(jìn)入正題晌该,可能以前我們并不關(guān)心內(nèi)存空間肥荔,從而導(dǎo)致對內(nèi)存泄露、深淺拷貝等知識(shí)點(diǎn)的理解有點(diǎn)模糊朝群。我的JS主軸線就是從內(nèi)存分配開始燕耿。

ps: 圖片看不到的,請用chrome姜胖、FF誉帅、opera。

數(shù)據(jù)結(jié)構(gòu)與算法


原諒我標(biāo)題黨一把右莱,什么數(shù)據(jù)結(jié)構(gòu)與算法都來了蚜锨。哈哈哈...

其實(shí)我是想說所有的語言都是為了博數(shù)據(jù)一笑而烽火戲程序猿,數(shù)據(jù)的存取當(dāng)然不容忽視慢蜓,我想從數(shù)據(jù)住的大房子來開始我的重新認(rèn)識(shí)JS之旅亚再。

這樣的大房子(內(nèi)存空間),在所有的編程語言中都擁有相似的生命周期:

  1. 我愛你晨抡,我給你一棟大房子(內(nèi)存分配)氛悬。
  2. 你懂的...(內(nèi)存使用: 讀、寫)
  3. 禁不起時(shí)間的考驗(yàn)凄诞,我要收回大房子,不歡而散忍级。(內(nèi)存釋放--"垃圾回收")

JS作為一門高級(jí)中的VIP的語言帆谍。在創(chuàng)建變量的時(shí)候會(huì)為其分配內(nèi)存空間,分配內(nèi)存的舉動(dòng)是在值的初始化轴咱、函數(shù)調(diào)用等階段完成汛蝙。在程序中,使用值的過程其實(shí)就是對值的內(nèi)存空間進(jìn)行寫入和讀取朴肺。

最后窖剑,不再使用的內(nèi)存空間會(huì)被自動(dòng)的進(jìn)行"垃圾回收"。但是確定一個(gè)分配的內(nèi)存空間是不是不再使用確實(shí)讓人頭疼戈稿,而且自動(dòng)一詞讓很多人不再關(guān)注于"垃圾回收"西土,這恰恰是一個(gè)美麗的錯(cuò)誤!

我的JS梳理路線第一波:

所以我們需要了解但是不限于以下知識(shí)點(diǎn):

  1. 內(nèi)存是什么?
  2. 堆('heap')
  3. 棧('stack')
  4. 隊(duì)列('queue')
  5. 基本類型與引用傳遞
  6. 深淺拷貝
  7. 垃圾回收
  8. 內(nèi)存泄露
  9. chrome工具進(jìn)行內(nèi)存分析

內(nèi)存是什么?


硬件上計(jì)算機(jī)存儲(chǔ)器由大量的觸發(fā)器組成鞍盗,觸發(fā)器包含了一些晶體管需了。每個(gè)觸發(fā)器可以存儲(chǔ)1bit(也叫做"位")。觸發(fā)器有唯一標(biāo)識(shí)用來尋址般甲,因此我們得以讀取或者覆蓋它們肋乍。

觸發(fā)器的組合形成更大的單位,比如8bit為1個(gè)字節(jié)(byte)敷存,還有kb...

我們可以抽象理解計(jì)算機(jī)的整個(gè)內(nèi)存是一個(gè)巨大的數(shù)組墓造。

靜態(tài)內(nèi)存分配和動(dòng)態(tài)內(nèi)存分配

對于原始數(shù)據(jù)類型:

int a; // 4個(gè)字節(jié)
int b[4]; // 4 * 4個(gè)字節(jié)
double c; // 8 個(gè)字節(jié)

編譯器會(huì)檢查數(shù)據(jù)類型并且提前計(jì)算出所需的空間大小(4+4*4+8)。然后為這些原始數(shù)據(jù)變量分配空間,分配的空間我們稱為"椕倜觯空間"帝雇。假如這些變量定義在一個(gè)函數(shù)中,當(dāng)函數(shù)被調(diào)用的時(shí)候谱煤,它們的內(nèi)存就加入到現(xiàn)有的內(nèi)存中摊求,函數(shù)調(diào)用終止,它們就會(huì)被移除刘离。

編譯器能夠準(zhǔn)確知道上面每一個(gè)原始數(shù)據(jù)變量的地址室叉,并且在插入與操作系統(tǒng)交互的代碼的同時(shí)在棧上為其它們申請對應(yīng)字節(jié)數(shù)的空間。這個(gè)過程就是靜態(tài)內(nèi)存分配硫惕,也有稱之為"自動(dòng)分配"茧痕。

如果操作b[4],因?yàn)檫@個(gè)元素并不存在恼除,因?yàn)閿?shù)組長度為4踪旷。所以最終可能讀取(重寫)到c的位。從而導(dǎo)致一些bug豁辉。

又如果:

int n = someFuncReturnN(...)

編譯器并不能提前的計(jì)算出變量所需的空間大小,而是在運(yùn)行的時(shí)候才能確定的令野,這個(gè)時(shí)候不能在棧上為其分配空間了,所以這個(gè)內(nèi)存是分配在堆('heap')空間上的徽级。

堆內(nèi)存涉及指針操作气破。不再贅述....說多了我就懵了。

靜態(tài)內(nèi)存分配和動(dòng)態(tài)內(nèi)存的區(qū)別:

  1. 靜態(tài)內(nèi)存分配:

    • 編譯期知道所需內(nèi)存空間大小餐抢。
    • 編譯期執(zhí)行
    • 申請到椣质梗空間
    • FILO(先進(jìn)后出)
  2. 動(dòng)態(tài)內(nèi)存分配:

    • 編譯期不知道所需內(nèi)存空間大小
    • 運(yùn)行期執(zhí)行
    • 申請到堆空間
    • 沒有特定的順序

總之說那么多,還不如一句話:

stack是采用靜態(tài)內(nèi)存分配的內(nèi)存空間旷痕,由系統(tǒng)自行釋放碳锈。heap是采用動(dòng)態(tài)內(nèi)存分配的內(nèi)存空間,無序欺抗,大小不定售碳,不會(huì)自動(dòng)釋放,哪怕你退出程序绞呈,那一塊內(nèi)存還是在那兒团滥。

堆('heap')


臥槽,前邊講多了报强,這里不知道說啥了灸姊。反正根據(jù)前邊說的動(dòng)態(tài)分配和靜態(tài)分配我們可以知道:

在JavaScript中,引用類型數(shù)據(jù)(對象秉溉、數(shù)組力惯、函數(shù))碗誉,這么說不太準(zhǔn)確,數(shù)組和函數(shù)也是對象父晶,就這么地吧哮缺。

它們都是申請到堆空間的,然后有一個(gè)引用甲喝,可以理解為一個(gè)指針尝苇,它保存了這個(gè)對象在堆中的位置。這個(gè)引用是存到棧中的埠胖。

棧('stack')


也叫堆棧糠溜。基本數(shù)據(jù)類型String直撤,Boolean之類的變量是申請到椃歉停空間的。

隊(duì)列('queue')


之前看過一個(gè)段子:

棧和隊(duì)列的區(qū)別? --吃多了拉就是隊(duì)列谋竖,吃多了吐就是棧红柱。

這特么也太有才了。不過說明了棧和隊(duì)列的特點(diǎn): 前者先入后出蓖乘、后者先入先出锤悄。

基本類型與引用傳遞


搞清楚內(nèi)存空間,再遇到這種面試題就不會(huì)瑟瑟發(fā)抖了嘉抒。

var a = 30;
var b = a;
b = 30;
// a是多少?

var obj = {a: 20, b:30}
var newObj = obj;
newObj.a = 25;
// obj.a是多少?

沒啥說的零聚,前者a,b都在棧空間申請了內(nèi)存众眨,var b=a的時(shí)候分配了新的值握牧。兩者互不相干容诬。

后邊的是引用傳遞娩梨,兩者指向堆內(nèi)存空間的某個(gè)位置的同一個(gè)對象。所以對對象的操作是互相影響的览徒。

深淺拷貝


淺拷貝:可以理解為只拷貝了1層狈定,如果有數(shù)組之類的對象的話,實(shí)際是拷貝了其引用习蓬。所以操作該對象是互相影響的纽什。內(nèi)存上是兩個(gè)引用指向了堆空間中的同一對象

var o = {
    name: 'jack ma',
    friends: ['李彥宏', '馬化騰']
}

var c = Object.assign({}, o);

c.friends.push('雷軍');
o.friends; // ["李彥宏", "馬化騰", "雷軍"]

深拷貝: 就是遞歸的拷貝,把屬性值也拷貝了躲叼÷郑互不影響了。內(nèi)存上是兩個(gè)引用分別指向了堆空間中的不同對象枫慷,但是初始值是一樣的让蕾。

var o = {
    name: 'jack ma',
    friends: ['李彥宏', '馬化騰']
}

var c = JSON.parse(JSON.stringify(o))

c.friends.push('雷軍');
o.friends; // ["李彥宏", "馬化騰"]

垃圾回收


垃圾回收是JS自動(dòng)完成的浪规,但是不代表我們就不去關(guān)注它。實(shí)際上確定一個(gè)內(nèi)存不再被使用探孝,然后將其釋放是很難的笋婿。通常有以下幾種算法實(shí)現(xiàn),但是也有很大的局限性顿颅。

  1. 引用計(jì)數(shù)垃圾收集算法

    這個(gè)算法是最簡單的缸濒,假如一個(gè)對象沒有指針指向它,那它就被認(rèn)為是可回收的粱腻。

    下面是MDN上面的例子:

    var o = { 
      a: {
        b:2
      }
    }; 
    // 兩個(gè)對象被創(chuàng)建庇配,一個(gè)作為另一個(gè)的屬性被引用,另一個(gè)被分配給變量o
    // 很顯然栖疑,沒有一個(gè)可以被垃圾收集
    
    
    var o2 = o; // o2變量是第二個(gè)對“這個(gè)對象”的引用
    
    o = 1;      // 現(xiàn)在讨永,“這個(gè)對象”的原始引用o被o2替換了
    
    var oa = o2.a; // 引用“這個(gè)對象”的a屬性
    // 現(xiàn)在,“這個(gè)對象”有兩個(gè)引用了遇革,一個(gè)是o2卿闹,一個(gè)是oa
    
    o2 = "yo"; // 最初的對象現(xiàn)在已經(jīng)是零引用了
               // 他可以被垃圾回收了
               // 然而它的屬性a的對象還在被oa引用,所以還不能回收
    
    oa = null; // a屬性的那個(gè)對象現(xiàn)在也是零引用了
               // 它可以被垃圾回收了
    

    這種算法的局限性體現(xiàn)在循環(huán)引用

    function f() {
      var o1 = {};
      var o2 = {};
      o1.p = o2; // o1 references o2
      o2.p = o1; // o2 references o1. This creates a cycle.
    }
    
    f();
    

    這樣垃圾收集器會(huì)認(rèn)為對象至少會(huì)被引用一次萝快,而不會(huì)回收這塊內(nèi)存锻霎。導(dǎo)致內(nèi)存泄露。

  2. 標(biāo)記-清除算法

    這個(gè)算法是現(xiàn)在瀏覽器基本都有的揪漩,其核心思想就是不能被引用的對象可被回收旋恼。

    原理大致是:

    1. 有一個(gè)GC root列表,保存了引用的全局變量奄容,比如 "window".
    2. root被認(rèn)為是活動(dòng)的冰更,不被回收,然后遞歸檢查其子節(jié)點(diǎn)昂勒,可以被訪問的都標(biāo)記為活動(dòng)的蜀细。
    3. 所有的不被標(biāo)記的,都是可回收的戈盈。

    [圖片上傳失敗...(image-9cf24e-1511490949760)]

    這樣的話奠衔,上面的循環(huán)引用,在函數(shù)結(jié)束后塘娶,o1,o2不再被全局變量所能訪問的對象引用归斤。就會(huì)被認(rèn)為是垃圾

內(nèi)存泄露


首先GC是無法預(yù)測的,其實(shí)回收更多的是取決于我們自己怎么去寫程序刁岸≡嗬铮或多或少年少的我們寫的代碼都導(dǎo)致了一些內(nèi)存無法被釋放,造成了內(nèi)存的泄露虹曙。

常見的內(nèi)存泄露


以下都是copy的經(jīng)典例子迫横。

  1. 全局變量

    根據(jù)上邊的標(biāo)記-清除算法鸦难,root列表中的全局變量是不會(huì)被釋放的。所以我們的代碼中顯式的全局或者隱式的全局變量是不會(huì)被垃圾收集器回收的员淫。

    隱式的情況全局變量有(還有很多):

    1. 忘記寫聲明了合蔽。

      function foo(){
          boss = 'jack ma'
      }
      foo();
      window.boss; // "jack ma"
      

      引擎對boss進(jìn)行LHS查詢,在當(dāng)前作用域沒有找到聲明介返,就去外層也就是全局之中找拴事,也特么沒找到,這個(gè)時(shí)候它就會(huì)發(fā)善心圣蝎,給你創(chuàng)建一個(gè)聲明刃宵。所以輸出window.boss是上面的結(jié)果。

      避免這種情況的辦法就是'use strict'徘公。

    2. this的默認(rèn)綁定規(guī)則

      function foo(){
          this.bar = 'jack ma'
      }
      foo();
      window.boss; // "jack ma"
      

      獨(dú)立的函數(shù)聲明采用的是默認(rèn)綁定規(guī)則牲证,也就說this是綁定到全局的。
      采用'use strict'可以是默認(rèn)綁定到undefined关面。

  2. 被遺忘的時(shí)光 | 回憶

    定時(shí)器我們常常使用坦袍。

    var serverData = loadData();
    setInterval(function() {
        var renderer = document.getElementById('renderer');
        if(renderer) {
            renderer.innerHTML = JSON.stringify(serverData);
        }
    }, 5000);
    

    IE6時(shí)代,假如serverData有大量的數(shù)據(jù)等太,它是沒辦法被收集的捂齐。但是現(xiàn)代瀏覽器在這個(gè)問題已經(jīng)做了優(yōu)化,無需擔(dān)心缩抡。

  3. 閉包

    var theThing = null;
    var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
        if (originalThing) // a reference to 'originalThing'
          console.log("hi");
      };
      theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
          console.log("message");
        }
      };
    };
    setInterval(replaceThing, 1000);
    

    一旦具有相同父作用域的多個(gè)閉包的作用域被創(chuàng)建奠宜,則這個(gè)作用域就可以被共享。
    也就是說為someMethod創(chuàng)建的作用域是被unused共享的瞻想。

    theThing作為root持有對someMethod的引用压真,unused引用的originalThing,也迫使其不會(huì)被回收蘑险。

    這個(gè)問題是Meteor小組發(fā)現(xiàn)的滴肿,有興趣可以百度。

  4. 脫離DOM的引用

    var elements = {
        button: document.getElementById('button'),
        image: document.getElementById('image')
    };
    function doStuff() {
        elements.image.src = 'http://example.com/image_name.png';
    }
    function removeImage() {
        // 刪除了DOM樹中對 image 的引用
        document.body.removeChild(document.getElementById('image'));
        // 但是GC并不會(huì)回收漠其。因?yàn)閑lements還引用了呀嘴高!
    }
    

chrome工具進(jìn)行內(nèi)存分析


利用瀏覽器進(jìn)行內(nèi)存分析具體步驟請執(zhí)行點(diǎn)擊下面的參考最后兩個(gè)竿音。

我們以上邊的閉包為例:

還有各種size之類的我就不說了和屎。反正chrome強(qiáng)大的一比!

參考

<a >MDN</a></br>
<a >How JavaScript works: memory management + how to handle 4 common memory leaks</a></br>
<a >Tracing garbage collection</a></br>
<a >ruanyf blog</a></br>
<a >chrome工具進(jìn)行內(nèi)存分析</a>

下一章

<a href='executionContext.md'>執(zhí)行上下文</a>

結(jié)語

擼主實(shí)力有限春瞬,高手歷來在民間柴信,希望廣提意見,補(bǔ)腎感激宽气。歡迎star随常,對我也是一種鼓勵(lì)潜沦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市绪氛,隨后出現(xiàn)的幾起案子唆鸡,更是在濱河造成了極大的恐慌,老刑警劉巖枣察,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件争占,死亡現(xiàn)場離奇詭異,居然都是意外死亡序目,警方通過查閱死者的電腦和手機(jī)臂痕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來猿涨,“玉大人握童,你說我怎么就攤上這事∨炎” “怎么了澡绩?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長俺附。 經(jīng)常有香客問我英古,道長,這世上最難降的妖魔是什么昙读? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任召调,我火速辦了婚禮,結(jié)果婚禮上蛮浑,老公的妹妹穿的比我還像新娘唠叛。我一直安慰自己,他們只是感情好沮稚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布艺沼。 她就那樣靜靜地躺著,像睡著了一般蕴掏。 火紅的嫁衣襯著肌膚如雪障般。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天盛杰,我揣著相機(jī)與錄音挽荡,去河邊找鬼。 笑死即供,一個(gè)胖子當(dāng)著我的面吹牛定拟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逗嫡,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼青自,長吁一口氣:“原來是場噩夢啊……” “哼株依!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起延窜,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤恋腕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后逆瑞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吗坚,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年呆万,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了商源。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谋减,死狀恐怖牡彻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情出爹,我是刑警寧澤庄吼,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站严就,受9級(jí)特大地震影響总寻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜梢为,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一渐行、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧铸董,春花似錦祟印、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至悲幅,卻和暖如春套鹅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背汰具。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工卓鹿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人郁副。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓减牺,卻偏偏與公主長得像豌习,于是被迫代替她去往敵國和親存谎。 傳聞我的和親對象是個(gè)殘疾皇子拔疚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355