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

1. 內(nèi)存空間

波比小金剛
本文地址

前言


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

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

為了面試或者裝逼,常常從言語(yǔ)不可描述的角度去看待這門語(yǔ)言,本身無(wú)可厚非。

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

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

ps: 圖片看不到的,請(qǐng)用chrome、FF、opera。

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


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

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

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

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

JS作為一門高級(jí)中的VIP的語(yǔ)言。在創(chuàng)建變量的時(shí)候會(huì)為其分配內(nèi)存空間,分配內(nèi)存的舉動(dòng)是在值的初始化、函數(shù)調(diào)用等階段完成。在程序中,使用值的過(guò)程其實(shí)就是對(duì)值的內(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í)用來(lái)尋址,因此我們得以讀取或者覆蓋它們。

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

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

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

對(duì)于原始數(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í)在棧上為其它們申請(qǐng)對(duì)應(yīng)字節(jié)數(shù)的空間。這個(gè)過(guò)程就是靜態(tài)內(nèi)存分配,也有稱之為"自動(dòng)分配"。

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

又如果:

int n = someFuncReturnN(...)

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

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

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

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

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

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

總之說(shuō)那么多,還不如一句話:

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

堆('heap')


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

在JavaScript中,引用類型數(shù)據(jù)(對(duì)象、數(shù)組、函數(shù)),這么說(shuō)不太準(zhǔn)確,數(shù)組和函數(shù)也是對(duì)象,就這么地吧。

它們都是申請(qǐng)到堆空間的,然后有一個(gè)引用,可以理解為一個(gè)指針,它保存了這個(gè)對(duì)象在堆中的位置。這個(gè)引用是存到棧中的。

棧('stack')


也叫堆棧。基本數(shù)據(jù)類型String,Boolean之類的變量是申請(qǐng)到棧空間的。

隊(duì)列('queue')


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

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

這特么也太有才了。不過(guò)說(shuō)明了棧和隊(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是多少?

沒(méi)啥說(shuō)的,前者a,b都在棧空間申請(qǐng)了內(nèi)存,var b=a的時(shí)候分配了新的值。兩者互不相干。

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

深淺拷貝


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

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

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

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

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

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è)算法是最簡(jiǎn)單的,假如一個(gè)對(duì)象沒(méi)有指針指向它,那它就被認(rèn)為是可回收的。

    下面是MDN上面的例子:

    var o = { 
      a: {
        b:2
      }
    }; 
    // 兩個(gè)對(duì)象被創(chuàng)建,一個(gè)作為另一個(gè)的屬性被引用,另一個(gè)被分配給變量o
    // 很顯然,沒(méi)有一個(gè)可以被垃圾收集
    
    
    var o2 = o; // o2變量是第二個(gè)對(duì)“這個(gè)對(duì)象”的引用
    
    o = 1;      // 現(xiàn)在,“這個(gè)對(duì)象”的原始引用o被o2替換了
    
    var oa = o2.a; // 引用“這個(gè)對(duì)象”的a屬性
    // 現(xiàn)在,“這個(gè)對(duì)象”有兩個(gè)引用了,一個(gè)是o2,一個(gè)是oa
    
    o2 = "yo"; // 最初的對(duì)象現(xiàn)在已經(jīng)是零引用了
               // 他可以被垃圾回收了
               // 然而它的屬性a的對(duì)象還在被oa引用,所以還不能回收
    
    oa = null; // a屬性的那個(gè)對(duì)象現(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)為對(duì)象至少會(huì)被引用一次,而不會(huì)回收這塊內(nèi)存。導(dǎo)致內(nèi)存泄露。

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

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

    原理大致是:

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

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

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

內(nèi)存泄露


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

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


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

  1. 全局變量

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

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

    1. 忘記寫聲明了。

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

      引擎對(duì)boss進(jìn)行LHS查詢,在當(dāng)前作用域沒(méi)有找到聲明,就去外層也就是全局之中找,也特么沒(méi)找到,這個(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ī)則,也就說(shuō)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ù),它是沒(méi)辦法被收集的。但是現(xiàn)代瀏覽器在這個(gè)問(wèn)題已經(jīng)做了優(yōu)化,無(wú)需擔(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è)作用域就可以被共享。
    也就是說(shuō)為someMethod創(chuàng)建的作用域是被unused共享的。

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

    這個(gè)問(wèn)題是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樹(shù)中對(duì) image 的引用
        document.body.removeChild(document.getElementById('image'));
        // 但是GC并不會(huì)回收。因?yàn)閑lements還引用了呀!
    }
    

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


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

我們以上邊的閉包為例:

還有各種size之類的我就不說(shuō)了。反正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é)語(yǔ)

擼主實(shí)力有限,高手歷來(lái)在民間,希望廣提意見(jiàn),補(bǔ)腎感激。歡迎star,對(duì)我也是一種鼓勵(lì)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,772評(píng)論 3 422
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,947評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,201評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,960評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,350評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,549評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,104評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,914評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,089評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,340評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,753評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 36,007評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,834評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,106評(píng)論 2 375