js——堆棧及簡單的瀏覽器底層運行機制


  數據結構是計算機存儲,組織數組的方式。數據結構是指相互之間存在一種或多種特定關系的數據元素的集合。數組結構的分類:數組、棧、堆、隊列、鏈表、樹、圖、散列表,本文主要用到和介紹的是棧和堆。

  一、定義

    棧(Stack)又名堆棧,它作為一種數據結構,是一種只能在一端進行插入和刪除操作的特殊線性表。它按照先進后出的原則存儲數據,先進入的數據被壓入棧底,最后的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據。棧具有記憶作用,對棧的插入與刪除操作中,不需要改變棧底指針。

    棧是允許在同一端進行插入和刪除操作的特殊線性表。允許進行插入和刪除操作的一端稱為棧頂(top),另一端為棧底(bottom);棧底固定,而棧頂浮動;棧中元素個數為零時稱為空棧。插入一般稱為進棧(PUSH),刪除則稱為退棧(POP)。

    堆(Heap)通常是一個可以被看做一棵完全二叉樹的數組對象。每個節點有一個值,堆中某個節點的值總是不大於或不小於其父節點的值。常用來實現優先隊列,堆的存取方式跟順序沒有關系,無序存取,根據引用直接獲取。

    內存一般指的是計算機的隨機存儲器(RAM)程序一般在此運行。JS內存空間分為棧(stack)、堆(heap)、池(一般也會歸類為棧中)。 其中棧存放變量,堆存放復雜對象,池存放常量,所以也叫常量池。

    基本類型:保存在棧內存中,因為這些類型在內存中分別占有固定大小的空間,通過按值來訪問。基本類型:Undefined、Null、Boolean、Number 、String。大致來說棧內存有如下特點:存儲基本數據類型,按值訪問,存儲的值大小固定,系統自動分配和釋放空間,主要是用來執行程序,空間小運行效率高,先進后出,后進先出

    引用類型 :如對象,數組,函數等它們是通過拷貝和new出來的,保存在堆內存中。因為這種值的大小不固定,因此不能把它們保存到棧內存中,因此保存在堆內存中,其實,說存儲於堆中不太准確,因為引用類型的數據的地址指針是存儲於棧中的,當我們想要訪問引用類型的值,需要先從棧中獲得對象的地址指針然后在通過地址指針找到堆中的所需要的數據。堆內存的特點:存儲引用數據類型,按引用訪問,存儲的值大小不定,可動態調整,手動分配和釋放空間,主要用來存放對象,空間大,運行效率較低,是一種無序的存儲,可根據引用直接獲取。 

    基本數據類型:某本數據類型的值保存在棧內存中,訪問方式是按值訪問,從一個變量向一個變量復制時,會在棧中創建一個新值,然后把值復制到為新變量分配的位置上。

    <script>
        //基本數據類型的賦值就是把值復制了一下
        var n = 2; var n1 = n ; console.log(n, n1); //2 2

        //復雜數據類型的賦值,它不光復制了值,還復制了內存中的引用地址
        var arr1 = [10, 'davina', true, null]; var arr2 = arr1; var str1 = arr1[3]; arr2.push('amy'); //arr1與arr2的引用地址是相同的所以不論修改了哪個,兩個都會一起改變
 console.log(arr2); // [10, "davina", true, null, "amy"]
 console.log(arr1); // [10, "davina", true, null, "amy"]
 arr2 = [2, 3, 4]; //因為arr2又重新賦值了,所以又開辟了一塊內存,引用地址就不一樣
 console.log(arr2); // [2, 3, 4]
 console.log(arr1); // [10, "davina", true, null, "amy"]
 console.log(str1); //null
 str1 = 6; console.log(arr1[3]); //null
    </script>

    引用數據類型:從上面的代碼可以看出,當改變arr2時,arr1中的數據也發生了變化。這是因為arr1是數組屬於引用類型,所以它賦值給arr2的時候傳的是棧中的地址(相當於新建了一個不同名“指針”),而不是堆內存中對象的值。arr1和arr2都指向同一塊堆內存,arr2修改堆內存時,也會影響到arr1。當arr2重新賦值時,又開辟了一塊內存,arr2的引用指向這塊新的內存,但arr1的指向並沒有發生改變,所以這時arr2和arr1又不一樣了。

    str1得到的是一個基本的數據類型的賦值,所以str1只是從arr1堆內存中獲取了一個數值並且直接保存在了棧中,,str1是直接在棧中修改,並不能直接影響到arr1堆內存的數據。

  二、瀏覽器中js代碼運行

  瀏覽器想要代碼執行就要提供一個供代碼執行的環境。我們把這個環境叫做執行環境棧ECStack。瀏覽器還會把內置的一些屬性和方法放到一個單獨的堆內存中這個堆內存叫做全局對象(Global Object)簡稱GO,以后供我們調用。瀏覽器會讓window指向GO,所以在瀏覽器端window代表的就是全局對象。

  瀏覽器環境提供后,我們開始讓代碼執行,代碼執行會有一個自己的執行上下文,那什么是執行上下文呢?

  執行上下文(Execution Context),簡稱EC,定義了變量或者函數有權訪問的其它數據。簡單來說指的就是當前代碼的執行環境。它分為全局執行上下文,我們在此簡稱為EC(G)、函數環境中的私有執行上下文和塊級執行上下文。每一個執行環境都是有一個與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中, 這個變量對象稱之為VO(Variable Object)。函數私有上下文中叫做AO(Activation Object)活動對象,但它也是變量對象,它是VO的一個分支。

  形成的全局執行上下文要進入到棧內存中執行,這個過程稱為‘進棧’。執行完代碼后它還有可能會有一個出棧釋放的步驟,它遵循一下先進后出的原則。在全局執行上下文中會創建一些全局變量,這些全局變量還有全局變量存放的值放在變量對象VO(G)中。 

  然后代碼自上而下執行。先創建一個值,在創建的時候,如果是基本數據類型的值,它可以直接存在棧內存中,如果是引用數據類型值要重新開辟一個堆內存,把內容存入,最后把這個堆內存16進制地址放入棧內存中,供變量關聯使用;然后創建相應的變量,最后把值和變量進行關聯(所有的指針賦值都是指針關聯指向)。
        let a  = 12; let b = a; b = 13; console.log(a); let n = { name:'davina' } let m = n; m.name = 'lisa'; console.log(n.name);
    var a = {n:12};
    var b = a;
    a.x = a = {n:13};
    console.log(a.x);
    console.log(a);

  

  二、生命周期

    不管什么程序,內存生命周期分為以下三步:1、分配所需要的內存  2、使用分配到的內存  3、不需要時將其釋放(歸放)。

    js環境中分配的內存有如下聲明周期:

      a.  內存分配:聲明變量,函數,對象的時候,系統會自動為他們分配內存

      b.  內存使用: 即讀寫內存,也就是使用變量,函數

      c.  內存回收: 由垃圾回收機制自動回收不再使用內存 

    js的內存分配:一般來說,js在定義變量時就完成了內存的分配。

    <script>
        var n = 2; //給數值變量分配內存
        var s = 'davina'    //給字符分配內存
        var o = { //給對象及其包含的值分配內存
 a: 1, b: 2 } function fn(c) { return c; //給函數分配內存
 } //有些函數調用結果是分配對象內存
        var d = new Date() // 分配一個Date對象
    </script>

    變量的生命周期:一個變量與人一樣都會有生命周期的,從定義到不再使用就是這個變量的生命周期,如果變量的生命周期已經到頭,那這個變量就會被垃圾回收機制回收然后釋放它的內存,局部變量的生命周期在函數執行完畢后就會結束,全局變量的生命周期在頁面關閉后才結束。

    js的內存使用:使用值的過程實際上是對分配內存進行讀取與寫入的操作,讀取與寫入可能是寫入一個變量或者一個對象的屬性值,甚至傳遞函數的參數。

    <script>
        var n = 2; //給數值變量分配內存
 console.log(n) //對內存的使用
    </script>

    js中的內存分為堆內存和棧內存:簡單來說堆內存存儲引用數據類型值,堆內存中,如果讓引用堆內存空間地址的變量變為null,那么就會達到堆內存釋放。棧內存,提供代碼執行的環境和存儲基本類型值,一般情況下,當函數執行完成,所形成的棧內存都會釋放掉(存儲的值也會釋放掉)但是也有一此特殊的情況:1、函數執行完成后,當前棧形成的棧內存中,某些內容被棧內存以外的變量占用了,這時棧內存就不以釋放2、全局棧內存只有在頁面關閉的時候才會釋放掉。如果當前棧內存沒有被釋放,那么之前要棧內存存儲的基本值也不會被釋放能一直保存下來。

  三、垃圾回收機制

    垃圾回收機制一般有以下幾點:

    1、用來釋放內存

    2、當一個數據使用完成后,垃圾回收機制會檢測這個數據有沒有在其它地方被引用 ,或者說是在其它的地方有沒有在使用,如果沒有使用的話就被回收,並釋放它占用的內存,如果在其它地方依然有使用,那就不會被回收

    3、垃圾回收機制是自動的,會定期去檢查數據的使用情況

    4、回收策略

      a.  標記清除(常用)

      標記清楚是將“不在使用的對象”定義為“無法到達的對象”。即從根部(在js中就是全局對象)出發定時掃描內存中的對象,凡是能從根部到達的對象進行保留,從根部出發無法觸及到的對象被標記為不在使用,后面進行回收。

      步驟:

      垃圾回收器創建一個名為“roots”列表,roots通常是代碼中的全局變量的引用(js中window對象是一個全局變量被當成root,window對象總是存在的,所以垃圾回收器可以檢查它和它的子對象是否存在)。

      所有roots被檢查和標記,所有的子對象也被標查,從root開始的所有對象如果可以到達它不會被當成垃圾

      所有標記清除的內存會被當成垃圾,進行回收。

    <script>
        function fn() { var a = 10; //進行標記
 a++; a = null; //標記清除
 } </script>

      

      b. 引用計數(不常用)

      記錄每個數據被使用的次數,當聲明一個變量並將引用類型賦值給該變量的時候這個值的引用次數就加1,如果這個變量的值變成了另外一個,則這個值的引用次數減1.當引用次數沒有變為0的時候就會造成內存泄漏,這個策略沒有辦法想到引用的次數。

      IE瀏覽器就是引用計數來進行內存管理的,在真實的項目中,某些情況會導致計數規則出現一些問題,造成很多內存不能被釋放掉,產生“內存泄漏”。

    <script>
        //引用計數
        var person = { age: 18, //標記1次
 name: 'davina' }; person.age = null; //雖然age設置了null,但是person對象還有指向name的引用,所以name不會被回收

        var p = person; person = 2; //原來的person對象被賦值為2,但因為有新引用p指向person對象,所以它不會被回收
 p = null       //person對象已經沒有引用了,所以會被回收
    </script>

   引用計數有一個問題那就是循環引用,如果兩個對象想到引用盡管不在使用,但不是被回收,可能導致內存泄露。

    <script>
        function fn() { var a = {}; var b = {}; a.age = b; b.age = a;
       return 'abc'; } fn();
</script>     

    5、內存泄漏

    程序的運行是需要用到內存的,對於持續運行的服務進程來說,必須及時的釋放內存保證系統的正常運行。本質上來說,內存泄漏是由於疏忽或者是錯誤造成程序未能釋放那些已經不在使用的內存,造成內存的浪費。

    如果避免:不用的東西及時歸還,減少不避要的全局變量。使用數據后及時解除引用(閉包,定時器等)避免死循環等等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM