JavaScript的垃圾收集機制

小夥伴們,很久不見最近偷懶一直沒有更新文章,今天我就給你們分享一下,我在紅皮書上看到的「JavaScript的垃圾回收機制」前端

  • 做用域java

  • 垃圾收集c++

    • 標記清除
    • 引用計數
    • 性能問題
    • 管理內存

小夥伴們我們想要透徹的瞭解垃圾回收機制,那麼我們就須要知道爲何要有存在垃圾回收機制,垃圾回收機制是用來解決的什麼,帶着這些疑問我們看下面的文章。web

做用域算法

什麼是執行環境? 執行環境是JavaScript中最爲重要的一個概念,執行環境定義了變量或者函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有一個與之關聯的變量對象。環境中定義的全部變量和函數都保存在這個對象中。雖然咱們編寫代碼的時候沒法訪問這個對象,可是解析器在處理數據時會在後臺使用這個變量。數組

全局執行環境是最外圍的一個執行環境,根據ECMAScript實現所在的宿主環境不一樣,表示執行環境的對象也不同。在web瀏覽器全局被認爲是window對象,所以全部的全局變量和函數都是做爲window對象的屬性和方法建立的。某個執行環境中的全部代碼執行完畢後,該環境被銷燬,保存在其中全部變量和函數定義隨之被銷燬(當瀏覽關閉或者關閉網頁的時候window就會被銷燬)。瀏覽器

每一個函數都有本身的執行環境。當執行流進入一個函數時,函數的環境會被推入一個環境棧中。這個環境棧中就是做用域,而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境。安全

當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈,做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數是有序的訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是函數,則將其活動對象做爲變量對象。活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。做用域鏈中的下一個變量對象來自包含(外部)環境,而在下一個變量對象則來自下一個包含環境。這樣一種延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。bash

簡單來講其實做用域鏈是大的做用域中嵌套小的做用域,小的做用域嵌套更小的做用域,最小的做用域能夠訪問它的父級做用域裏面的變量和方法,可是不能夠訪問父級同級的做用域,簡單來講在最小的一個做用域中去查找當前最小做用域中沒有的變量,那麼就會去最小做用域父級的環境中去查找,若是能找到就直接使用,若是沒有的話接着去父級的父級去查找,直到window前端最大做用域下去查找,查不到的話,就會出問題(在使用的狀況下,賦值的就會在window下面直接添加一個對象,且不會報錯),可是父級的不能訪問子級的變量函數

const color = 'blue';
const changeColor =()=>{
    const processColor={
        'red':'blue',
        'blue':'red'
    }
    return processColor(color)||''
}
console.log("Color is now " + changeColor());
複製代碼

在這個簡單示例中,函數changeColor()的做用域包含兩個對象:它本身的變量對象(自己自帶arguments對象)和全局環境的變量對象。能夠在函數內部訪問外部變量color,就是由於能夠在這個做用域中找到它。

此外,在局部定義的變量能夠和全局的變量相互使用,以下面這個實例:

let color='blue'
const changeColor =()=>{
    const autherColor = 'red';
    const swapColor = ()=>{
        let tempColor = autherColor;
        aitherColor = color;
        //這是最小的做用域 能夠訪問父級全部變量
    }
    // 這裏能夠訪問父級 color變量和本身的autherColor、swapColor變量
    swapColor()
}
changeColor()
這裏只能訪問color

複製代碼

以上代碼共涉及3個執行環境: 全局環境、changeColor的局部環境和swapColor()的局部環境。全局環境中有一個變量Color和一個函數changeColor()。changeColor()的局部環境中包含autherColor變量和swapColor函數,但它能夠訪問全局環境中的變量color,swapColor()的局部環境只有一個變量tempColor,該變量只能在本環境中訪問到。不管是全局環境仍是changeColor的局部環境都無權訪問tempColor,然而swapColor能夠訪問其餘兩個環境變量,由於這個全局和changeColor都是swapChange的父級環境。看下面做用域鏈圖

上面的矩形圖表示特定的執行環境。其中,內部環境能夠經過作做用域鏈訪問全部外部環境的變量,可是外部的不能訪問內部的任何變量和函數。這些環境之間的聯繫是線性、有次序的。每一個環境均可以向上搜索做用域鏈,來查找變量和函數名;可是任何環境不能經過向下搜索做用域鏈而進入另外一個執行環境。 函數參數也被當作變量來對待,所以其訪問規則和環境變量相同

沒有塊級做用域

JavaScript沒有塊級做用域常常致使理解上的困惑。在其餘類c的語言中,又花括號封閉的代碼塊都有本身的做用域,於是支持根據條件來定於變量。例如,下面的代碼在JavaScript中並不會獲得想要的結果

if(true){
    var color = 'blue'
}
alert(color) // blue 調用alert都會作一操做 "變量.toString()"" 複製代碼

這裏是一個if語句中定義了變量color。若是在c、c++或者java中,color會在if語句執行完畢被銷燬。可是在javaScript中,if語句中的變量會將變量添加到當前的執行環境中。在使用for語句時尤爲要牢記一差別,例如

for(var i=0;i<10;i++){
    dosomething(i);
}
alert(color)// 10 調用alert都會作一操做 "變量.toString()"" 複製代碼

對於塊級做用域的語言來講,for語句初始化變量的表達式所定義的變量,只會存在在於循環的環境變量中。而JavaScript來講,由for語句建立的變量i即便在for結束後,也依舊存在於循環外部的執行環境

1.聲明變量

使用var聲明變量會自動被添加到最近的環境中。在函數內部,最近的環境就是函數的局部環境;若是初始化變量時沒有使用var聲明,該變量就會自動被添加到全局環境,以下

function add (num1,num2){
    var sum=num1+num2;
    return sum;
}
var result  = add (10,20)// 30
alert(sum) // 因爲sum不是有效的變量,會致使錯誤
複製代碼

以上代碼中的函數add()定義了sum局部變量,該變量包含加法操做的結果。雖然結果值從函數中返回了,但變量sum在函數外部訪問不到的。若是省略這個例子中的var關鍵字,那麼當add()執行完畢後,那麼sum也將能夠訪問到;

function add(num1,num2){
    sum = num1+num2;
    return sum;
}
var result = add(10,20);//30
alert(sum);//30
複製代碼

這樣例子中的變量sum在被初始化賦值時沒有使用var關鍵字。因而,當調用完add()以後,添加到全局環境的變量sum將繼續存在;即便函數已經執行完畢,後面的代碼依舊能夠訪問它。在編寫JavaScript代碼過程當中,不聲明而直接初始化變量是一個常見的錯誤作法,由於會致使意外。咱們建議在初始化變量以前,必定要先聲明,這樣就能夠避免相似問題。在嚴格模式下,初始化未經聲明的變量會致使錯誤

查詢標識符

當中某個環境中爲了讀取或寫入而引入標識符時,必須經過搜索來肯定該標識符實際表明什麼。搜索過程從做用域的前端開始,(當前做用域最底層),向上逐級查詢與給定名字匹配的標識符。若是在局部環境中找到該標識符,搜索中止,變量就緒,若是局部環境中沒有找到該變量名,則繼續沿做用域鏈上搜索。搜索過程將一直追溯到全局環境的變量對象。若是全局環境中也沒有找到這個標識符,則意味着該變量還沒有聲明

經過下面示例,能夠理解查詢標識符的過程:

var color = 'blue';
function getColor(){
    retur color;
}
console.log(getColor().toString())//blue
console.log(getColor().toString())===alert(getColor())
複製代碼

調用本例中的函數getColor()時會引用變量color。爲了肯定color的值,將開始一個兩步的搜索過程。首先,搜索getColor的變量對象,查找其中是否包含一個名爲color的標識符。在沒有找到的狀況下,搜索繼續到下一個變量對象(全局變量的變量對象),而後在哪裏找到了名爲color的標識符,搜索過程宣佈結束。

在搜索過程當中,若是存在一個局部的變量定義,則就會中止搜索,不會在進入下一個變量對象。若是在當前的局部變量找到就不會去父級環境中查找

垃圾回收集

JavaScript 具備自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程當中使用的內存。而在c、c++之類的語言中,開發人員的一項基本的任務就是手動跟蹤內存的使用狀況,這是創造許多問題的一個根源。在編寫JavaScript程序時,開發人員不用在擔憂內存問題,所需內存的分配以及無用內存的回收徹底實現了自動管理。這種垃圾回收機制的緣由含簡單:找到那些不在繼續使用的變量,而後釋放佔其內存。爲此,垃圾回收集器會按照固定的時間間隔(或代碼執行中預約的收集時間),週期性的執行這一操做。

下面咱們來分析一下函數中局部變量的生命週期。局部變量只在函數執行的過程當中存在。而這個過程當中,會爲局部變量在棧(或堆)內存上分配相應的空間,以便存儲它們的值。而後在函數中使用這些變量,直到函數執行結束。此時,局部變量就沒有存在的必要;單非全部的狀況下都這麼容易就能得出結論。垃圾回收器必須跟蹤那個變量有用那個變量沒有用,對於不在有用的變量打上標記,以備未來收回其佔用的內存。因爲標識無用變量的策略會致使異常,可是瀏覽器中的實現,則一般有兩個策略。

標記清楚

JavaScript中最經常使用的垃圾收集方式是標記清楚(mark-and-sweep)。當變量進入環境(例如,在函數中聲明一個變量)時,就將變量標記爲「進入環境」。邏輯上講,永遠不能釋放進入環境的變量所佔的內存,由於只要執行流入相應的環境,就有可能會用到它們,而變量離開環境時,將其標註爲「離開環境」。

可使用任何的方式來標記變量。好比,能夠經過翻轉某個特殊的位來記錄一個變量發生的變化。說到底,如何標記一個變量其實並不重要,關鍵的在於採用什麼策略。

垃圾收集器在運行的時候會給存在內存的全部變量都加上標記(固然,可使用任何標記方式)。而後,它會去掉環境中的變量以及被環境中的變量引用的變量標記。而在此以後再被加上標記的變量將視爲準備刪除的變量,緣由是環境中的變量已經沒法訪問到這個變量了。最後,垃圾收集器完成內存清楚的工做,銷燬那些帶標記的值並回收它們所佔用的內存空間。2008年爲止,IE、Firefox、Opera、Chrome和Safari的JavaScript實現使用的都是標記清楚方式的垃圾收集策略,只不過垃圾收集的時間間隔有所不一樣

引用計數

另外一種不太常見的垃圾收集策略叫作引用計數(reference counting)。 引用計數的含義是跟蹤記錄每一個值被引用的次數。當聲明一個變量並將一個引用類型的值賦給變量時,則這個值的引用次數就是1.若是同一個值被賦值給另外一個變量,則該值的引用次數加1.相反,若是包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦法再次訪問這個值了,於是就能夠將其佔用的內存空間回收回來。這樣,當垃圾收集器下次運行的時,它就會釋放那些引用次數爲0的值所佔用的內存。

Netscape Navigator3.0 是最先使用引用技術策略的瀏覽器,可是很快就發現一個嚴重的問題:循環引用。循環引用指的是對象A中包含對象B的指針,而對象B中也包含一個指針對象A的引用。看下面的例子

function problem(){
    var objectA = new Object();
    var objectB = new Object();
    objectA.someOtherObject = objectB;
    obhectB.someOtherObject = objectA;
}
複製代碼

在這個例子中,objectA 和 objectB 經過各自的屬性相互引用;也就是說,這兩個對象的引用次數都是2.在採用標記清楚策略的實現中,因爲函數執行後,這兩個對象都離開了做用域,所以這種相互引用不是問題,可是採用計數策略的實現中,當函數執行後,objectA和objectB還將繼續存在,由於他們的引用次數永遠不會是0.加入這個函數被重複屢次調用,就會致使大量的內存得不到回收。爲此。Netscape Navigator4.0 放棄了引用計數方式,轉而採用了標記清楚來實現其垃圾回收機制,但是引用計數致使的麻煩並未就此終結。

咱們知道,IE中有一部分對象並非原生JavaScript對象,例如, BOM、DOM中的對象就是使用c++以COM(Component Object Model ,組件對象模型)對象的形式實現的,而COM對象的垃圾回收集機制採用的就是引用計數策略。所以,即便IE的JavaScript引擎使用標記清除策略來實現的,單數JavaScript訪問COM對象依然是基於計數策略的。只要是涉及到COM對象的使用就有可能存在循環引用的問題,爲了解決避免這個循環引用的問題,在IE9一下的版本不要是相似一下的操做

var element = document.getElementById('some_elelment');
var myObject = new Object();
myObject.element = element;
element.someObject =element 
// 若是使用最好有一下操做 ,手動清楚
myObject=null;
element=null;
複製代碼

爲了解決上面的問題 IE9把BOM和DOM對象都轉換成了真正的JavaScript對象,這樣就避免了兩種垃圾收集算法並存致使的問題,也消除了常見的內存泄露。

性能問題

垃圾收集器是週期性運行的,並且若是變量分配的內存數量很可觀,那麼回收工做也是至關大的。這種狀況下,肯定垃圾收集時間間隔是一個很是重要的問題。說道垃圾收集器多長時間運行一次,不由讓人聯繫到IE所以而名聲狼藉的性能問題。IE的垃圾收集器是根據內存分配運行的,具體一點就是256個變量,4096個對象(或者數組)字面量和數組元素(slot)或者64kb的字符串。達到上述任何一個臨界值,垃圾收集器就會運行。這種實現方式的問題在於,若是一個腳本包含那麼多變量,那麼該腳本頗有可能會在其生命週期中一直保有那麼多的變量。這樣一來,垃圾收集器不得不頻繁地運行。結果,由此引起的嚴重性能問題促使IE7從新寫了其垃圾收集例程。

隨着IE7的發佈,JavaScript引擎的垃圾收集例程改變了工做方式:觸發垃圾收集的變量分配、字面量和數組元素的臨界值被調整爲動態修正。IE7中的各項臨界值在初始化的與IE6相等。若是垃圾收集例程回收的內存分配量低於15%,則變量、字面量和數組元素的臨界就會加倍。若是回收例程在85%的內存分配量,則將各類臨界值重置到默認值。這一看似簡單的調整,也極大地提高了IE在運行包含大量JavaScript的頁面時的性能,實際上,在有的瀏覽器上能夠觸發垃圾收集過程,但咱們不建議讀者這樣作。在IE中,調用window.CollectGarbage()方法就會當即執行垃圾收集。在Opera7以及更高的版本中,調用window.opera.collect()也會啓動垃圾收集歷程

管理內存

使用具有垃圾收集機制的語言編寫程序,開發人員通常沒必要操心內存管理的問題,可是,JavaScript在進行內存管理及垃圾收集時面臨的問題仍是有點不同凡響的。其中一個主要的問題,就是分配web瀏覽器可用內存的數量一般要比分配桌面應用程序少。這樣作的目的主要是出於安全方面的考慮,目的是防止運行JavaScript的網頁耗盡所有系統內存而致使系統崩潰。內存限制問題不只會影響給變量分配內存,同時還影響調用棧以及在一個線程中可以同時執行的語句數量。

所以,確保佔用最少的內存可讓頁面得到更好的性能。而優化內存佔用的最佳方式,就是爲執行中的代碼只保存必要的數據。一單不在使用,最好使用null來釋放其引用----這個叫作解除引用(dereferencing)。這一作法適用於大多數全局變量和全局對象。局部變量會在它們離開執行環境時自動被解除引用,以下面的例子:

function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
var globalPerson = createPerson('Nicholas');
// 手動解除 globalPerson的引用
globalPerson = null;
複製代碼

在這個例子中,變量globalPerson取得了createPerson()函數返回的值。在createPerson()函數內部,咱們建立了一個對象並將其賦給局部變量localPerson,而後又爲該對象添加一個屬性name。最後,當調用這個函數時,localPerson以函數值的形式返回並賦值給全局變量globalPerson。因爲localPerson在createPerson()執行完畢後就離開了其執行環境,所以無需咱們顯式地去爲它解除引用。可是對於全局變量globalPerson而言,則須要咱們在不使用它的時候手工爲它解釋引用,這也是上面例子中最後一行代碼的目的。

不過,解除一個值的引用並不意味着自動回收該值所佔用的內存。解除引用的真正做用是讓值脫離執行環境,以便垃圾收集器下次運行時將其收回。

小結 JavaScript變量可用來保存兩種類型的值:基本類型值和應用類型值,基本類型的值源自如下5中基本類型:Undefined、Null、Boolean、Number、String。基本類型值和引用類型值有一下特色:

  • 基本類型值在內存中佔據固定大小的空間,所以保存在棧內存中
  • 從一個變量向另外一個變量複製基本類型的值,會建立這個值的副本;
  • 引用類型的值是對象,保存在堆內存中中;
  • 包含引用類型值的變量實際上包含的並非對象的自己,而是一個指向該對象的指針;
  • 從一個變量向另外一個變量複製引用類型的值,複製的實際上是指針,所以兩個變量最終指向同一個對象
相關文章
相關標籤/搜索