首頁  >  文章  >  web前端  >  JavaScript隱藏機制之垃圾回收知識總結

JavaScript隱藏機制之垃圾回收知識總結

WBOY
WBOY轉載
2022-06-08 16:11:262637瀏覽

這篇文章為大家帶來了關於javascript的相關知識,其中主要介紹了垃圾回收的相關問題,垃圾回收是JavaScript的隱藏機制,下面一起來看一下,希望對大家有幫助。

JavaScript隱藏機制之垃圾回收知識總結

【相關推薦:javascript影片教學web前端

一、前言

#垃圾回收是JavaScript的隱藏機制,我們通常不需要為垃圾回收勞心費力,只需要專注功能的開發就好了。但這並不意味著我們在寫JavaScript的時候就可以高枕無憂了,伴隨著我們實現的功能越來越複雜,程式碼量越積越大,效能問題就變的越來越突出。如何寫出執行速度更快,而且佔用記憶體更小的程式碼是程式設計師永無止境的追求。優秀的程式設計師總是能在極為有限的資源下,達到驚人的效果,這也正式芸芸眾生和高高在上的神祗之間的差異。

二、何為垃圾

程式碼執行在電腦的記憶體中,我們在程式碼中定義的所有變數、物件、函數都會在記憶體中佔用一定的記憶體空間。在電腦中,記憶體空間是非常緊張的資源,我們必須時時刻刻注意記憶體的佔用量,畢竟記憶體條非常貴!如果一個變數、函數或物件在創建之後不再被後繼的程式碼執行所需要,那麼它就可以被稱為垃圾。

雖然從直覺上理解垃圾的定義非常容易,但是對於一個電腦程式來說,我們很難在某一時刻斷定當前存在的變數、函數或物件在未來不再使用。為了降低電腦記憶體的開銷,同時確保電腦程式正常執行,我們通常規定滿足以下任一條件的物件或變數為垃圾:

  1. 沒有被引用的物件或變數;
  2. 無法存取的物件(多個物件之間循環引用);

沒有被引用的變數或物件相當於一座沒有門的房子,我們永遠都無法進入其中,因此不可能在用到它們。無法存取的物件之間雖然具備連通性,但是仍然無法從外部進入其中,因此也無法再次被利用。滿足以上條件的物件或變量,在程式未來執行過程中絕對不會再次被採用,因此可以放心的當作垃圾回收。

當我們透過上述定義明確了需要丟棄的對象,是否就代表剩餘的變數、物件中就沒有垃圾了呢?

不是的!我們目前分辨出的垃圾只是所有垃圾的一部分,仍然會有其他垃圾不符合以上條件,但是也不會再被使用了。

這是否可以說滿足以上定義的垃圾是“絕對垃圾”,其他隱藏在程式中的為“相對垃圾”呢?

三、垃圾回收

垃圾回收機制(GC,Garbage Collection)負責在程式執行過程中回收無用的變數和記憶體佔用的空間。一個物件雖然沒有再次使用的可能,但是仍然存在於記憶體中的現像被稱為記憶體洩漏。記憶體洩漏是非常危險的現象,尤其在長時間運行的程式中。如果一個程式出現了記憶體洩漏,它佔用的記憶體空間就會越來越多,直到耗盡記憶體。

字串、物件和陣列沒有固定的大小,所以只有當它們大小已知時才能對它們進行動態的儲存分配。 JavaScript程式每次建立字串、陣列或物件時,解釋器都要分配記憶體才儲存這個實體。只要像這樣動態地分配了內存,最終都要釋放這些內存以便它們能夠被再次利用;否則,JavaScript的解釋器將會消耗完系統中所有可用的內存,造成系統崩潰。

JavaScript的垃圾回收機制會間歇性的檢查沒有用途的變數、物件(垃圾),並釋放條它們所佔用的空間。

四、可達性(Reachability)

不同的程式語言採用不同的垃圾回收策略,例如C 就沒有垃圾回收機制,所有的記憶體管理靠程式設計師本身的技能,這也就造成了C 比較難以掌握的現況。 JavaScript採用可達性管理內存,從字面意思來看,可達的意思是可以到達,也就是指程式可以透過某種方式存取、使用的變數和對象,這些變數所佔用的記憶體是不可以被釋放的。

JavaScript規定了一個固有的可達值集合,集合中的值天生就是可達的:

  1. 目前正在執行的函數上下文(包括函數內的局部變數、函數的參數等);
  2. 目前巢狀呼叫鏈上的其他函數、它們的局部變數和參數;
  3. 全域變數;
  4. 其他內部的變數;

以上變數稱為#​​##根,是可達性樹的頂層節點。

如果一個變數或則對象,直接或間接的被根變數應用,則認為這個變數是可達的。

換一個說法,如果一個值能夠透過根訪問到(例如,

A.b.c.d.e),那麼這個值就是可達的。

五、可達性舉例

層次關聯:
let people = {
    boys:{
        boys1:{name:'xiaoming'},
        boys2:{name:'xiaojun'},
    },
    girls:{
        girls1:{name:'xiaohong'},
        girls2:{name:'huahua'},
    }};

以上程式碼建立了一個對象,並賦值給了變數

people ,變數people中包含了兩個物件boysgirlsboysgirls中又分別包含了兩個子物件。這也就建立了一個包含了3層引用關係的資料結構(不考慮基礎型別資料的情況),如下圖:

JavaScript隱藏機制之垃圾回收知識總結

其中,

people節點由於是全域變量,所以天然可達。 boysgirls節點由於被全域變數直接引用,構成間接可達。 boys1boys2girls1girls2由於被全域變數間接應用,可以透過people.boys.boys訪問,因此也屬於可達變數。

如果我們在上述程式碼的後面加上以下程式碼:

people.girls.girls2 = null;people.girls.girls1 = people.boys.boys2;
那麼,以上引用層次圖將會變成如下形式:

JavaScript隱藏機制之垃圾回收知識總結

其中,

girls1girls2由於和grils節點斷開連接,從而變成了不可達節點,意味著將被垃圾回收機制回收。

而如果此時,我們再執行以下程式碼:

people.boys.boys2 = null;
那麼引用層次圖將變成如下形式:


JavaScript隱藏機制之垃圾回收知識總結

此時,雖然

boys節點和boys2節點斷開了連接,但由於boys2節點和girls節點之間存在引用關係,所以boys2仍屬於可達的,不會被垃圾回收機制回收。

以上關聯關係圖證明了為何稱全域變數等值為

,因為在關聯關係圖中,這一類別值通常會作為關係樹的根節點出現。

相互關聯:

let people = {
    boys:{
        boys1:{name:'xiaoming'},
        boys2:{name:'xiaojun'},
    },
    girls:{
        girls1:{name:'xiaohong'},
        girls2:{name:'huahua'},
    }};people.boys.boys2.girlfriend = people.girls.girls1;	
    //boys2引用girls1people.girls.girls1.boyfriend = people.boys.boys2;	//girls1引用boys2

以上程式碼在

boys2girls1之間創建了一個相互關聯的關係,關係結構圖如下:

JavaScript隱藏機制之垃圾回收知識總結

此時,如果我們切斷

boysboys2之間的關聯:

delete people.boys.boys2;
物件之間的關聯關係圖如下:

JavaScript隱藏機制之垃圾回收知識總結

#顯然,並沒有不可達的節點出現。

此時,如果我們切斷

boyfriend關係連結:

delete people.girls.girls1;
關係圖變成:

JavaScript隱藏機制之垃圾回收知識總結##此時,雖然

boys2

girls1之間還存在著girlfriend關係,但是,boys2以及變成不可達節點,將被垃圾回收機制收回。

可達孤島:

let people = {
    boys:{
        boys1:{name:'xiaoming'},
        boys2:{name:'xiaojun'},
    },
    girls:{
        girls1:{name:'xiaohong'},
        girls2:{name:'huahua'},
    }};delete people.boys;delete people.girls;
以上程式碼所形成的引用層次圖如下:

JavaScript隱藏機制之垃圾回收知識總結此時,雖然虛線框內部的物件之間仍然存在著相互引用的關係,但是這些物件同樣是不可達的,並且會被垃圾回收機制刪除。這些節點已經和

脫離了關係,變的不可達。 六、垃圾回收演算法

引用計數

所謂引用計-數,顧名思義,就是每次物件被引用時都會計數,增加引用就加一,刪除引用就減一,如果引用數變成0,那麼就被認定為垃圾,從而刪除物件回收記憶體。

舉個例子:

let user = {username:'xiaoming'};
//对象被user变量引用,计数+1
let user2 = user;
//对象被新的变量引用,计数+1
user = null;
//变量不再引用对象,计数-1
user2 = null;
//变量不再引用对象,奇数-1
//此时,对象引用数为0,会被删除

雖然看起來引用計數方法非常合理,但實際上,採用引用計數方法的記憶體回收機制存在明顯的漏洞。

例如:

let boy = {};	
let girl = {};	
boy.girlfriend = girl;
girl.boyfriend = boy;
boy = null;
girl = null;

以上代码在boygirl之间存在相互引用,计数删掉boygirl内的引用,二者对象并不会被回收。由于循环引用的存在,两个匿名对象的引用计数永远不会归零,也就产生了内存泄漏。

C++中存在一个智能指针shared_ptr)的概念,程序员可以通过智能指针,利用对象析构函数释放引用计数。但是对于循环引用的状况就会产生内存泄漏。

好在JavaScript已经采用了另外一种更为安全的策略,更大程度上避免了内存泄漏的风险。

标记清除

标记清除mark and sweep)是JavaScript引擎采取的垃圾回收算法,其基本原理是从出发,广度优先遍历变量之间的引用关系,对于遍历过的变量打上一个标记(优秀员工徽章),最后删除没有标记的对象。

算法基本过程如下:

  1. 垃圾收集器找到所有的,并颁发优秀员工徽章(标记);
  2. 然后它遍历优秀员工,并将优秀员工引用的对象同样打上优秀员工标记;
  3. 反复执行第2步,直至无新的优秀员工加入;
  4. 没有被标记的对象都会被删除。

举个栗子:

如果我们程序中存在如下图所示的对象引用关系:

JavaScript隱藏機制之垃圾回收知識總結

我们可以清晰的看到,在整个图片的右侧存在一个“可达孤岛”,从出发,永远无法到达孤岛。但是垃圾回收器并没有我们这种上帝视角,它们只会根据算法会首先把根节点打上优秀员工标记。

JavaScript隱藏機制之垃圾回收知識總結

然后从优秀员工出发,找到所有被优秀员工引用的节点,如上图中虚线框中的三个节点。然后把新找到的节点同样打上优秀员工标记。

JavaScript隱藏機制之垃圾回收知識總結

反复执行查找和标记的过程,直至所有能找到的节点都被成功标记。

JavaScript隱藏機制之垃圾回收知識總結

最终达到下图所示的效果:

JavaScript隱藏機制之垃圾回收知識總結

由于在算法执行周期结束之后,右侧的孤岛仍然没有标记,因此会被垃圾回收器任务无法到达这些节点,最终被清除。

如果学过数据结构和算法的童鞋可能会惊奇的发现,这不就是图的遍历吗,类似于连通图算法。

七、性能优化

垃圾回收是一个规模庞大的工作,尤其在代码量非常大的时候,频繁执行垃圾回收算法会明显拖累程序的执行。JavaScript算法在垃圾回收上做了很多优化,从而在保证回收工作正常执行的前提下,保证程序能够高效的执行。

性能优化采取的策略通常包括以下几点:

分代回收

JavaScript程序在执行过程中会维持相当量级的变量数目,频繁扫描这些变量会造成明显的开销。但是这些变量在生命周期上各有特点,例如局部变量会频繁的创建,迅速的使用,然后丢弃,而全局变量则会长久的占据内存。JavaScript把两类对象分开管理,对于快速创建、使用并丢弃的局部变量,垃圾回收器会频繁的扫描,保证这些变量在失去作用后迅速被清理。而对于哪些长久把持内存的变量,降低检查它们的频率,从而节约一定的开销。

增量收集

增量式的思想在性能优化上非常常见,同样可以用于垃圾回收。在变量数目非常大时,一次性遍历所有变量并颁发优秀员工标记显然非常耗时,导致程序在执行过程中存在卡顿。所以,引擎会把垃圾回收工作分成多个子任务,并在程序执行的过程中逐步执行每个小任务,这样就会造成一定的回收延迟,但通常不会造成明显的程序卡顿。

空閒收集

CPU即使是在複雜的程式中也不是一直都有工作的,這主要是因為CPU工作的速度非常快,外圍IO往往慢上幾個數量級,所以在CPU空閒的時候安排垃圾回收策略是一種非常有效的性能優化手段,而且基本上不會對程序本身造成不良影響。這種策略就類似系統的空閒時間升級一樣,使用者根本察覺不到後台的執行。

八、總結

本文的主要任務是簡單的結束垃圾回收的機制、常用的策略和優化的手段,並不是為了讓大家深入了解引擎的後台執行原理。

透過本文,你應該了解:

  1. 垃圾回收是JavaScript的特性之一,執行在後台,無需我們操心;
  2. 垃圾回收的策略是標記清除,按照可達性理論篩選並清除垃圾;
  3. 標記清楚策略可以避免可達孤島帶來的內存洩漏

【相關推薦:javascript影片教學web前端

以上是JavaScript隱藏機制之垃圾回收知識總結的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除