欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你了解JavaScript垃圾回收機(jī)制

 更新時(shí)間:2021年11月02日 15:07:15   作者:隱冬  
JS自帶一套內(nèi)存管理引擎,負(fù)責(zé)創(chuàng)建對(duì)象、銷毀對(duì)象,以及垃圾回收,下面這篇文章主要給大家介紹了關(guān)于JavaScript垃圾回收機(jī)制的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下

1. 概述

隨著軟件開發(fā)行業(yè)的不斷發(fā)展,性能優(yōu)化已經(jīng)是一個(gè)不可避免的話題,那什么樣的行為才能算得上是性能優(yōu)化呢?

本質(zhì)上任何一種可以提高運(yùn)行效率,降低運(yùn)行開銷的行為,都可以看做是一種優(yōu)化操作。

這也就意味著,在軟件開放行業(yè)必然存在著很多值得優(yōu)化的地方,特別是在前端開發(fā)過程中,性能優(yōu)化可以認(rèn)為是無處不在的。例如請(qǐng)求資源時(shí)所用到的網(wǎng)絡(luò),以及數(shù)據(jù)的傳輸方式,再或者開發(fā)過程中所使用到的框架等都可以進(jìn)行優(yōu)化。

本章探索的是JavaScript語言本身的優(yōu)化,是從認(rèn)知內(nèi)存空間的使用到垃圾回收的方式,從而可以編寫出高效的JavaScript代碼。

2. 內(nèi)存管理

隨著近些年硬件技術(shù)的不斷發(fā)展,高級(jí)編程語言中都自帶了GC機(jī)制,讓開發(fā)者在不需要特別注意內(nèi)存空間使用的情況下,也能夠正常的去完成相應(yīng)的功能開發(fā)。為什么還要重提內(nèi)存管理呢,下面就通過一段極簡(jiǎn)單的代碼來進(jìn)行說明。

首先定義一個(gè)普通的函數(shù)fn,然后在函數(shù)體內(nèi)聲明一個(gè)數(shù)組,接著給數(shù)組賦值,需要注意的是在賦值的時(shí)候刻意選擇了一個(gè)比較大的數(shù)字來作為下標(biāo)。這樣做的目的就是為了當(dāng)前函數(shù)在調(diào)用的時(shí)候可以向內(nèi)存盡可能多的申請(qǐng)一片比較大的空間。

function fn() {
    arrlist = [];
    arrlist[100000] = 'this is a lg';
}

fn()

在執(zhí)行這個(gè)函數(shù)的過程中從語法上是不存在任何問題的,不過用相應(yīng)的性能監(jiān)控工具對(duì)內(nèi)存進(jìn)行監(jiān)控的時(shí)候會(huì)發(fā)現(xiàn),內(nèi)存變化是持續(xù)程線性升高的,并且在這個(gè)過程當(dāng)中沒有回落。這代表著內(nèi)存泄露。如果在寫代碼的時(shí)候不夠了解內(nèi)存管理的機(jī)制就會(huì)編寫出一些不容易察覺到的內(nèi)存問題型代碼。

這種代碼多了以后程序帶來的可能就是一些意想不到的bug,所以掌握內(nèi)存的管理是非常有必要的。因此接下來就去看一下,什么是內(nèi)存管理。

從這個(gè)詞語本身來說,內(nèi)存其實(shí)就是由可讀寫的單元組成,他標(biāo)識(shí)一片可操作的空間。而管理在這里刻意強(qiáng)調(diào)的是由人主動(dòng)去操作這片空間的申請(qǐng)、使用和釋放,即使借助了一些API,但終歸可以自主的來做這個(gè)事。所以內(nèi)存管理就認(rèn)為是,開發(fā)者可以主動(dòng)的向內(nèi)存申請(qǐng)空間,使用空間,并且釋放空間。因此這個(gè)流程就顯得非常簡(jiǎn)單了,一共三步,申請(qǐng),使用和釋放。

回到JavaScript中,其實(shí)和其他的語言一樣,JavaScript中也是分三步來執(zhí)行這個(gè)過程,但是由于ECMAScript中并沒有提供相應(yīng)的操作API。所以JavaScript不能像C或者C++那樣,由開發(fā)者主動(dòng)調(diào)用相應(yīng)的API來完成內(nèi)存空間的管理。

不過即使如此也不能影響我們通過JavaScript腳本來演示一個(gè)空間的生命周期是怎樣完成的。過程很簡(jiǎn)單首先要去申請(qǐng)空間,第二個(gè)使用空間,第三個(gè)釋放空間。

在JavaScript中并沒有直接提供相應(yīng)的API,所以只能在JavaScript執(zhí)行引擎遇到變量定義語句的時(shí)候自動(dòng)分配一個(gè)相應(yīng)的空間。這里先定義一個(gè)變量obj,然后把它指向一個(gè)空對(duì)象。對(duì)它的使用其實(shí)就是一個(gè)讀寫的操作,直接往這個(gè)對(duì)象里面寫入一個(gè)具體的數(shù)據(jù)就可以了比如寫上一個(gè)yd。最后可以對(duì)它進(jìn)行釋放,同樣的JavaScript里面并沒有相應(yīng)的釋放API,所以這里可以采用一種間接的方式,比如直接把他設(shè)置為null。

let obj = {}

obj.name = 'yd'

obj = null

這個(gè)時(shí)候就相當(dāng)于按照內(nèi)存管理的一個(gè)流程在JavaScript當(dāng)中實(shí)現(xiàn)了內(nèi)存管理。后期在這樣性能監(jiān)控工具當(dāng)中看一下內(nèi)存走勢(shì)就可以了。

3. 垃圾回收

首先在JavaScript中什么樣的內(nèi)容會(huì)被當(dāng)中是垃圾看待。在后續(xù)的GC算法當(dāng)中,也會(huì)存在的垃圾的概念,兩者其實(shí)是完全一樣的。所以在這里統(tǒng)一說明。

JavaScript中的內(nèi)存管理是自動(dòng)的。每創(chuàng)建一個(gè)對(duì)象、數(shù)組或者函數(shù)的時(shí)候,就會(huì)自動(dòng)的分配相應(yīng)的內(nèi)存空間。等到后續(xù)程序代碼在執(zhí)行的過程中如果通過一些引用關(guān)系無法再找到某些對(duì)象的時(shí)候那么這些對(duì)象就會(huì)被看作是垃圾。再或者說這些對(duì)象其實(shí)是已經(jīng)存在的,但是由于代碼中一些不合適的語法或者說結(jié)構(gòu)性的錯(cuò)誤,沒有辦法再去找到這些對(duì)象,那么這種對(duì)象也會(huì)被稱之是垃圾。

發(fā)現(xiàn)垃圾之后JavaScript執(zhí)行引擎就會(huì)出來工作,把垃圾所占據(jù)的對(duì)象空間進(jìn)行回收,這個(gè)過程就是所謂的垃圾回收。在這里用到了幾個(gè)小的概念,第一是引用,第二是從根上訪問,這個(gè)操作在后續(xù)的GC里面也會(huì)被頻繁的提到。

在這里再提一個(gè)名詞叫可達(dá)對(duì)象,首先在JavaScript中可達(dá)對(duì)象理解起來非常的容易,就是能訪問到的對(duì)象。至于訪問,可以是通過具體的引用也可以在當(dāng)前的上下文中通過作用域鏈。只要能找得到,就認(rèn)為是可達(dá)的。不過這里邊會(huì)有一個(gè)小的標(biāo)準(zhǔn)限制就是一定要是從根上出發(fā)找得到才認(rèn)為是可達(dá)的。所以又要去討論一下什么是根,在JavaScript里面可以認(rèn)為當(dāng)前的全局變量對(duì)象就是根,也就是所謂的全局執(zhí)行上下文。

簡(jiǎn)單總結(jié)一下就是JavaScript中的垃圾回收其實(shí)就是找到垃圾,然后讓JavaScript的執(zhí)行引擎來進(jìn)行一個(gè)空間的釋放和回收。

這里用到了引用和可達(dá)對(duì)象,接下來就盡可能的通過代碼的方式來看一下在JavaScript中的引用與可達(dá)是怎么體現(xiàn)的。

首先定義一個(gè)變量,為了后續(xù)可以修改值采用let關(guān)鍵字定一個(gè)obj讓他指向一個(gè)對(duì)象,為了方便描述給他起一個(gè)名字叫

let obj = {name: 'xiaoming'}

寫完這行代碼以后其實(shí)就相當(dāng)于是這個(gè)空間被當(dāng)前的obj對(duì)象引用了,這里就出現(xiàn)了引用。站在全局執(zhí)行上下文下obj是可以從根上來被找到的,也就是說這個(gè)obj是一個(gè)可達(dá)的,這也就間接地意味著當(dāng)前xiaoming的對(duì)象空間是可達(dá)的。

接著再重新再去定義一個(gè)變量,比如ali讓他等于obj,可以認(rèn)為小明的空間又多了一次引用。這里存在著一個(gè)引用數(shù)值變化的,這個(gè)概念在后續(xù)的引用計(jì)數(shù)算法中是會(huì)用到的。

let obj = {name: 'xiaoming'}

let ali = obj

再來做一個(gè)事情,直接找到obj然后把它重新賦值為null。這個(gè)操作做完之后就可以思考一下了。本身小明這對(duì)象空間是有兩個(gè)引用的。隨著null賦值代碼的執(zhí)行,obj到小明空間的引用就相當(dāng)于是被切斷了。現(xiàn)在小明對(duì)象是否還是可達(dá)呢?必然是的。因?yàn)閍li還在引用著這樣的一個(gè)對(duì)象空間,所以說他依然是一個(gè)可達(dá)對(duì)象。

這就是一個(gè)引用的主要說明,順帶也看到了一個(gè)可達(dá)。

接下來再舉一個(gè)示例,說明一下當(dāng)前JavaScript中的可達(dá)操作,不過這里面需要提前說明一下。

為了方便后面GC中的標(biāo)記清除算法,所以這個(gè)實(shí)例會(huì)稍微麻煩一些。

首先定義一個(gè)函數(shù)名字叫objGroup,設(shè)置兩個(gè)形參obj1和obj2,讓obj1通過一個(gè)屬性指向obj2,緊接著再讓obj2也通過一個(gè)屬性去指向obj1。再通過return關(guān)鍵字直接返回一個(gè)對(duì)象,obj1通過o1進(jìn)行返回,再設(shè)置一個(gè)o2讓他找到obj2。完成之后在外部調(diào)用這個(gè)函數(shù),設(shè)置一個(gè)變量進(jìn)行接收,obj等于objGroup調(diào)用的結(jié)果。傳兩個(gè)參數(shù)分別是兩個(gè)對(duì)象obj1和obj2。

function objGroup(obj1, obj2) {
    obj1.next = obj2;
    obj2.prev = obj1;
}

let obj = objGroup({name: 'obj1'}, {name: 'obj2'});

console.log(obj);

運(yùn)行可以發(fā)現(xiàn)得到了一個(gè)對(duì)象。對(duì)象里面分別有obj1和obj2,而obj1和obj2他們內(nèi)部又各自通過一個(gè)屬性指向了彼此。

{
    o1: {name: 'obj1', next: {name: 'obj2', prev: [Circular]}},
    o2: {name: 'obj2', next: {name: 'obj1', next: [Circular]}}
}

分析一下代碼,首先從全局的根出發(fā),是可以找到一個(gè)可達(dá)的對(duì)象obj,他通過一個(gè)函數(shù)調(diào)用之后指向了一個(gè)內(nèi)存空間,他的里面就是上面看到的o1和o2。然后在o1和o2的里面剛好又通過相應(yīng)的屬性指向了一個(gè)obj1空間和obj2空間。obj1和obj2之間又通過next和prev做了一個(gè)互相的一個(gè)引用,所以代碼里面所出現(xiàn)的對(duì)象都可以從根上來進(jìn)行查找。不論找起來是多么的麻煩,總之都能夠找到,繼續(xù)往下來再來做一些分析。

如果通過delete語句把obj身上o1的引用以及obj2對(duì)obj1的引用直接delete掉。此時(shí)此刻就說明了現(xiàn)在是沒有辦法直接通過什么樣的方式來找到obj1對(duì)象空間,那么在這里他就會(huì)被認(rèn)為是一個(gè)垃圾的操作。最后JavaScript引擎會(huì)去找到他,然后對(duì)其進(jìn)行回收。

這里說的比較麻煩,簡(jiǎn)單來說就是當(dāng)前在編寫代碼的時(shí)候會(huì)存在的一些對(duì)象引用的關(guān)系,可以從根的下邊進(jìn)行查找,按照引用關(guān)系終究能找到一些對(duì)象。但是如果找到這些對(duì)象路徑被破壞掉或者說被回收了,那么這個(gè)時(shí)候是沒有辦法再找到他,就會(huì)把他視作是垃圾,最后就可以讓垃圾回收機(jī)制把他回收掉。

4. GC算法介紹

GC可以理解為垃圾回收機(jī)制的簡(jiǎn)寫,GC工作的時(shí)候可以找到內(nèi)存當(dāng)中的一些垃圾對(duì)象,然后對(duì)空間進(jìn)行釋放還可以進(jìn)行回收,方便后續(xù)的代碼繼續(xù)使用這部分內(nèi)存空間。至于什么樣的東西在GC里邊可以被當(dāng)做垃圾看待,在這里給出兩種小的標(biāo)準(zhǔn)。

第一種從程序需求的角度來考慮,如果說某一個(gè)數(shù)據(jù)在使用完成之后上下文里邊不再需要去用到他了就可以把他當(dāng)做是垃圾來看待。

例如下面代碼中的name,當(dāng)函數(shù)調(diào)用完成以后已經(jīng)不再需要使用name了,因此從需求的角度考慮,他應(yīng)該被當(dāng)做垃圾進(jìn)行回收。至于到底有沒有被回收現(xiàn)在先不做討論。

function func() {
    name = 'yd';
    return `${name} is a coder`
}

func()

第二種情況是當(dāng)前程序運(yùn)行過程中,變量能否被引用到的角度去考慮,例如下方代碼依然是在函數(shù)內(nèi)部放置一個(gè)name,不過這次加上了一個(gè)聲明變量的關(guān)鍵字。有了這個(gè)關(guān)鍵字以后,當(dāng)函數(shù)調(diào)用結(jié)束后,在外部的空間中就不能再訪問到這個(gè)name了。所以找不到他的時(shí)候,其實(shí)也可以算作是一種垃圾。

function func() {
    const name = 'yd';
    return `${name} is a coder`
}

func()

說完了GC再來說一下GC算法。我們已經(jīng)知道GC其實(shí)就是一種機(jī)制,它里面的垃圾回收器可以完成具體的回收工作,而工作的內(nèi)容本質(zhì)就是查找垃圾釋放空間并且回收空間。在這個(gè)過程中就會(huì)有幾個(gè)行為:查找空間,釋放空間,回收空間。這樣一系列的過程里面必然有不同的方式,GC的算法可以理解為垃圾回收器在工作過程中所遵循的一些規(guī)則,好比一些數(shù)學(xué)計(jì)算公式。

常見的GC算法有引用計(jì)數(shù),可以通過一個(gè)數(shù)字來判斷當(dāng)前的這個(gè)對(duì)象是不是一個(gè)垃圾。標(biāo)記清除,可以在GC工作的時(shí)候給那些活動(dòng)對(duì)象添加標(biāo)記,以此判斷它是否是垃圾。標(biāo)記整理,與標(biāo)記清除很類似,只不過在后續(xù)回收過程中,可以做出一些不一樣的事情。分代回收,V8中用到的回收機(jī)制。

5. 引用計(jì)數(shù)算法

引用計(jì)數(shù)算法的核心思想是在內(nèi)部通過引用計(jì)數(shù)器來維護(hù)當(dāng)前對(duì)象的引用數(shù),從而判斷該對(duì)象的引用數(shù)值是否為0來決定他是不是一個(gè)垃圾對(duì)象。當(dāng)這個(gè)數(shù)值為0的時(shí)候GC就開始工作,將其所在的對(duì)象空間進(jìn)行回收和釋放。

引用計(jì)數(shù)器的存在導(dǎo)致了引用計(jì)數(shù)在執(zhí)行效率上可能與其它的GC算法有所差別。

引用的數(shù)值發(fā)生改變是指某一個(gè)對(duì)象的引用關(guān)系發(fā)生改變的時(shí)候,這時(shí)引用計(jì)數(shù)器會(huì)主動(dòng)的修改當(dāng)前這個(gè)對(duì)象所對(duì)應(yīng)的引用數(shù)值。例如代碼里有一個(gè)對(duì)象空間,有一個(gè)變量名指向他,這個(gè)時(shí)候數(shù)值+1,如果又多了一個(gè)對(duì)象還指向他那他再+1,如果是減小的情況就-1。當(dāng)引用數(shù)字為0的時(shí)候,GC就會(huì)立即工作,將當(dāng)前的對(duì)象空間進(jìn)行回收。

通過簡(jiǎn)單的代碼來說明一下引用關(guān)系發(fā)生改變的情況。首先定義幾個(gè)簡(jiǎn)單的user變量,把他作為一個(gè)普通的對(duì)象,再定義一個(gè)數(shù)組變量,在數(shù)組的里存放幾個(gè)對(duì)象中的age屬性值。再定義一個(gè)函數(shù),在函數(shù)體內(nèi)定義幾個(gè)變量數(shù)值num1和num2,注意這里是沒有const的。在外層調(diào)用函數(shù)。

const user1 = {age: 11};
const user2 = {age: 22};
const user3 = {age: 33};

const nameList = [user1.age, user2.age, user3.age,];

function fn() {
    num1 = 1;
    num2 = 2;
}

fn();

首先從全局的角度考慮會(huì)發(fā)現(xiàn)window的下邊是可以直接找到user1,user2,user3以及nameList,同時(shí)在fn函數(shù)里面定義的num1和num2由于沒有設(shè)置關(guān)鍵字,所以同樣是被掛載在window對(duì)象下的。這時(shí)候?qū)@些變量而言他們的引用計(jì)數(shù)肯定都不是0。

接著在函數(shù)內(nèi)直接把num1和num2加上關(guān)鍵字的聲明,就意味著當(dāng)前這個(gè)num1和num2只能在作用域內(nèi)起效果。所以,一旦函數(shù)調(diào)用執(zhí)行結(jié)束之后,從外部全局的地方出發(fā)就不能找到num1和num2了,這個(gè)時(shí)候num1和num2身上的引用計(jì)數(shù)就會(huì)回到0。此時(shí)此刻只要是0的情況下,GC就會(huì)立即開始工作,將num1和num2當(dāng)做垃圾進(jìn)行回收。也就是說這個(gè)時(shí)候函數(shù)執(zhí)行完成以后內(nèi)部所在的內(nèi)存空間就會(huì)被回收掉。

const user1 = {age: 11};
const user2 = {age: 22};
const user3 = {age: 33};

const nameList = [user1.age, user2.age, user3.age,];

function fn() {
    const num1 = 1;
    const num2 = 2;
}

fn();

那么緊接著再來看一下其他的比如說user1,user2,user3以及nameList。由于userList,里面剛好都指向了上述三個(gè)對(duì)象空間,所以腳本即使執(zhí)行完一遍以后user1,user2,user3他里邊的空間都還被人引用著。所以此時(shí)的引用計(jì)數(shù)器都不是0,也就不會(huì)被當(dāng)做垃圾進(jìn)行回收。這就是引用計(jì)數(shù)算法實(shí)現(xiàn)過程中所遵循的基本原理。簡(jiǎn)單的總結(jié)就是靠著當(dāng)前對(duì)象身上的引用計(jì)數(shù)的數(shù)值來判斷是否為0,從而決定他是不是一個(gè)垃圾對(duì)象。

1. 引用計(jì)數(shù)優(yōu)缺點(diǎn)

引用計(jì)數(shù)算法的優(yōu)點(diǎn)總結(jié)出兩條。

第一是引用計(jì)數(shù)規(guī)則會(huì)在發(fā)現(xiàn)垃圾的時(shí)候立即進(jìn)行回收,因?yàn)樗梢愿鶕?jù)當(dāng)前引用數(shù)是否為0來決定對(duì)象是不是垃圾。如果是就可以立即進(jìn)行釋放。

第二就是引用計(jì)數(shù)算法可以最大限度的減少程序的暫停,應(yīng)用程序在執(zhí)行的過程當(dāng)中,必然會(huì)對(duì)內(nèi)存進(jìn)行消耗。當(dāng)前執(zhí)行平臺(tái)的內(nèi)存肯定是有上限的,所以內(nèi)存肯定有占滿的時(shí)候。由于引用計(jì)數(shù)算法是時(shí)刻監(jiān)控著內(nèi)存引用值為0的對(duì)象,舉一個(gè)極端的情況就是,當(dāng)他發(fā)現(xiàn)內(nèi)存即將爆滿的時(shí)候,引用計(jì)數(shù)就會(huì)立馬找到那些數(shù)值為0的對(duì)象空間對(duì)其進(jìn)行釋放。這樣就保證了當(dāng)前內(nèi)存是不會(huì)有占滿的時(shí)候,也就是所謂的減少程序暫停的說法。

引用計(jì)數(shù)的缺點(diǎn)同樣給出兩條說明。

第一個(gè)就是引用計(jì)數(shù)算法沒有辦法將那些循環(huán)引用的對(duì)象進(jìn)行空間回收的。通過代碼片段演示一下,什么叫做循環(huán)引用的對(duì)象。

定義一個(gè)普通的函數(shù)fn在函數(shù)體的內(nèi)部定義兩個(gè)變量,對(duì)象obj1和obj2,讓obj1下面有一個(gè)name屬性然后指向obj2,讓obj2有一個(gè)屬性指向obj1。在函數(shù)最后的地方return返回一個(gè)普通字符,當(dāng)然這并沒有什么實(shí)際的意義只是做一個(gè)測(cè)試。接著在最外層調(diào)用一下函數(shù)。

function fn() {
    const obj1 = {};
    const obj2 = {};

    obj1.name = obj2;
    obj2.name = obj1;

    return 'yd is a coder';
}

那么接下來分析還是一樣的道理,函數(shù)在執(zhí)行結(jié)束以后,他內(nèi)部所在的空間肯定需要有涉及到空間回收的情況。比如說obj1和obj2,因?yàn)樵谌值牡胤狡鋵?shí)已經(jīng)不再去指向他了,所以這個(gè)時(shí)候他的引用計(jì)數(shù)應(yīng)該是為0的。

但是這個(gè)時(shí)候會(huì)有一個(gè)問題,在里邊會(huì)發(fā)現(xiàn),當(dāng)GC想要去把obj1刪除的時(shí)候,會(huì)發(fā)現(xiàn)obj2有一個(gè)屬性是指向obj1的。換句話講就是雖然按照之前的規(guī)則,全局的作用域下找不到obj1和obj2了,但是由于他們兩者之間在作用域范圍內(nèi)明顯還有著一個(gè)互相的指引關(guān)系。這種情況下他們身上的引用計(jì)數(shù)器數(shù)值并不是0,GC就沒有辦法將這兩個(gè)空間進(jìn)行回收。也就造成了內(nèi)存空間的浪費(fèi),這就是所謂的對(duì)象之間的循環(huán)引用。這也是引用計(jì)數(shù)算法所面臨到的一個(gè)問題。

第二個(gè)問題就是引用計(jì)數(shù)算法所消耗的時(shí)間會(huì)更大一些,因?yàn)楫?dāng)前的引用計(jì)數(shù),需要維護(hù)一個(gè)數(shù)值的變化,在這種情況下要時(shí)刻的監(jiān)控著當(dāng)前對(duì)象的引用數(shù)值是否需要修改。對(duì)象數(shù)值的修改需要消耗時(shí)間,如果說內(nèi)存里邊有更多的對(duì)象需要修改,時(shí)間就會(huì)顯得很大。所以相對(duì)于其他的GC算法會(huì)覺得引用計(jì)數(shù)算法的時(shí)間開銷會(huì)更大一些。

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

相比引用計(jì)數(shù)而言標(biāo)記清除算法的原理更加簡(jiǎn)單,而且還能解決一些相應(yīng)的問題。在V8中被大量的使用到。

標(biāo)記清除算法的核心思想就是將整個(gè)垃圾回收操作分成兩個(gè)階段,第一個(gè)階段遍歷所有對(duì)象然后找到活動(dòng)對(duì)象進(jìn)行標(biāo)記?;顒?dòng)就像跟之前提到的可達(dá)對(duì)象是一個(gè)道理,第二個(gè)階段仍然會(huì)遍歷所有的對(duì)象,把沒有標(biāo)記的對(duì)象進(jìn)行清除。需要注意的是在第二個(gè)階段當(dāng)中也會(huì)把第一個(gè)階段設(shè)置的標(biāo)記抹掉,便于GC下次能夠正常工作。這樣一來就可以通過兩次遍歷行為把當(dāng)前垃圾空間進(jìn)行回收,最終再交給相應(yīng)的空閑列表進(jìn)行維護(hù),后續(xù)的程序代碼就可以使用了。

這就是標(biāo)記清除算法的基本原理,其實(shí)就是兩個(gè)操作,第一是標(biāo)記,第二是清除。這里舉例說明。

首先在全局global聲明A,B,C三個(gè)可達(dá)對(duì)象,找到這三個(gè)可達(dá)對(duì)象之后,會(huì)發(fā)現(xiàn)他的下邊還會(huì)有一些子引用,這也就是標(biāo)記清除算法強(qiáng)大的地方。如果發(fā)現(xiàn)他的下邊有孩子,甚至孩子下邊還有孩子,這個(gè)時(shí)候他會(huì)用遞歸的方式繼續(xù)尋找那些可達(dá)的對(duì)象,比如說D,E分別是A和C的子引用,也會(huì)被標(biāo)記成可達(dá)的。

這里還有兩個(gè)變量a1和b1,他們?cè)诤瘮?shù)內(nèi)的局部作用域,局部作用域執(zhí)行完成以后這個(gè)空間就被回收了。所以從global鏈條下是找不到a1和b1的,這時(shí)候GC機(jī)制就會(huì)認(rèn)為他是一個(gè)垃圾對(duì)象,沒有給他做標(biāo)記,最終在GC工作的時(shí)候就會(huì)把他們回收掉。

const A = {};

function fn1() {
    const D = 1;
    A.D = D;
}

fn1();

const B;

const C = {};

function fn2() {
    const E = 2;
    A.E = E;
}

fn2();

function fn3() {
    const a1 = 3;
    const b1 = 4;
}

fn3();

這就是標(biāo)記清除所謂的標(biāo)記階段和清除階段,以及這兩個(gè)階段分別要做的事情。簡(jiǎn)單的整理可以分成兩個(gè)步驟。在第一階段要找到所有可達(dá)對(duì)象,如果涉及到引用的層次關(guān)系,會(huì)遞歸進(jìn)行查找。找完以后會(huì)將這些可達(dá)對(duì)象進(jìn)行標(biāo)記。標(biāo)記完成以后進(jìn)行第二階段開始做清除,找到那些沒有做標(biāo)記的對(duì)象,同時(shí)還將第一次所做的標(biāo)記清除掉。這樣就完成了一次垃圾回收,同時(shí)還要留意,最終會(huì)把回收的空間直接放在一個(gè)叫做空閑列表上面。方便后續(xù)的程序可以直接在這申請(qǐng)空間使用。

1. 標(biāo)記清除算法優(yōu)缺點(diǎn)

相對(duì)比引用計(jì)數(shù)而言標(biāo)記清除具有一個(gè)最大的優(yōu)點(diǎn),就是可以解決對(duì)象循環(huán)引用的回收操作。在寫代碼的時(shí)候可能會(huì)在全局定義A、B、C這樣的可達(dá)對(duì)象,也會(huì)有一些函數(shù)的局部作用域,比如在函數(shù)內(nèi)定義了a1和b1,而且讓他們互相引用。

const A = {};

const B;

const C = {};

function fn() {
    const a1 = {};
    const b1 = {};
    a1.value = b1;
    b1.value = a1;
}

fn();

函數(shù)的調(diào)用在結(jié)束之后必然要去釋放他們內(nèi)部的空間,在這種情況下一旦當(dāng)某一個(gè)函數(shù)調(diào)用結(jié)束之后他局部空間中的變量就失去了與全局global作用域上的鏈接。這個(gè)時(shí)候a1和b1在global根下邊就沒辦法訪問到了,就是一個(gè)不可達(dá)的對(duì)象。不可達(dá)對(duì)象在做標(biāo)記階段的時(shí)候不能夠完成標(biāo)記,在第二個(gè)階段回收的時(shí)候就直接進(jìn)行釋放了。

這是標(biāo)記清除可以做到的,但是在引用計(jì)數(shù)里面,函數(shù)調(diào)用結(jié)束同時(shí)也沒有辦法在全局進(jìn)行訪問??墒怯捎诋?dāng)前判斷的標(biāo)準(zhǔn)是引用數(shù)字是否為0,在這種情況下,就沒有辦法釋放a1和b1空間,這就是標(biāo)記清除算法的最大優(yōu)點(diǎn),當(dāng)然這是相對(duì)于引用計(jì)數(shù)算法而言的。

同時(shí)標(biāo)記清除算法也會(huì)有一些缺點(diǎn)。比如模擬一個(gè)內(nèi)存的存儲(chǔ)情況,從根進(jìn)行查找,在下方有一個(gè)可達(dá)對(duì)象A對(duì)象, 左右兩側(cè)有一個(gè)從跟下無法直接查找的一個(gè)區(qū)域,B和C。這種情況下在進(jìn)行第二輪清除操作的時(shí)候,就會(huì)直接將B和C所對(duì)應(yīng)的空間進(jìn)行回收。然后把釋放的空間添加到空閑列表上,后續(xù)的程序可以直接從空閑列表上申請(qǐng)相應(yīng)的一個(gè)空間地址,進(jìn)行使用。在這種情況下就會(huì)有一個(gè)問題。

function fn() {
    const B = '兩個(gè)';
}
fn();

const A = '四個(gè)文字';

function fn2() {
    const C = '一個(gè)';
}
fn2();

比如我們認(rèn)為,任何一個(gè)空間都會(huì)有兩部分組成,一個(gè)用來存儲(chǔ)空間一些元信息比如他的大小,地址,稱之為頭。還有一部分是專門用于存放數(shù)據(jù)的叫做域,B、C空間認(rèn)為B對(duì)象有2個(gè)字的空間,C對(duì)象有1個(gè)字的空間。這種情況下,雖然對(duì)他進(jìn)行了回收,加起來好像是釋放了3個(gè)字的空間,但是由于它們中間被A對(duì)象去分割著。所以在釋放完成之后其實(shí)還是分散的也就是地址不連續(xù)。

這點(diǎn)很重要,后續(xù)想申請(qǐng)的空間地址大小剛好1.5個(gè)字。這種情況下,如果直接找到B釋放的空間會(huì)發(fā)現(xiàn)是多了的,因?yàn)檫€多了0.5個(gè),如果直接去找C釋放的空間又發(fā)現(xiàn)不夠,因?yàn)槭?個(gè)。所以這就帶來了標(biāo)記清除算法中最大的問題,空間的碎片化。

所謂的空間碎片化,就是由于當(dāng)前所回收的垃圾對(duì)象在地址上本身是不連續(xù)的,由于這種不連續(xù)從而造成了回收之后分散在各個(gè)角落,后續(xù)要想去使用的時(shí)候,如果新的生成空間剛好與他們的大小匹配,就能直接用。一旦是多了或是少了就不太適合使用了。

這就是標(biāo)記清除算法優(yōu)點(diǎn)和缺點(diǎn),簡(jiǎn)單的整理一下就是優(yōu)點(diǎn)是可以解決循環(huán)引用不能回收的問題,缺點(diǎn)是說會(huì)產(chǎn)生空間碎片化的問題,不能讓空間得到最大化的使用。

7. 標(biāo)記整理算法

在V8中標(biāo)記整理算法會(huì)被頻繁的使用到,下面來看一下是如何實(shí)現(xiàn)的。

首先認(rèn)為標(biāo)記整理算法是標(biāo)記清除的增強(qiáng)操作,他們?cè)诘谝粋€(gè)階段是完全一樣的,都會(huì)去遍歷所有的對(duì)象,然后將可達(dá)活動(dòng)對(duì)象進(jìn)行標(biāo)記。第二階段清除時(shí),標(biāo)記清除是直接將沒有標(biāo)記的垃圾對(duì)象做空間回收,標(biāo)記整理則會(huì)在清除之前先執(zhí)行整理操作,移動(dòng)對(duì)象的位置,讓他們能夠在地址上產(chǎn)生連續(xù)。

假設(shè)回收之前有很多的活動(dòng)對(duì)象和非活動(dòng)對(duì)象,以及一些空閑的空間,當(dāng)執(zhí)行標(biāo)記操作的時(shí)候,會(huì)把所有的活動(dòng)對(duì)象進(jìn)行標(biāo)記,緊接著會(huì)進(jìn)行整理的操作。整理其實(shí)就是位置上的改變,會(huì)把活動(dòng)對(duì)象先進(jìn)行移動(dòng),在地址上變得連續(xù)。緊接著會(huì)將活動(dòng)對(duì)象右側(cè)的范圍進(jìn)行整體的回收,這相對(duì)標(biāo)記清除算法來看好處是顯而易見的。

因?yàn)樵趦?nèi)存里不會(huì)大批量出現(xiàn)分散的小空間,從而回收到的空間都基本上都是連續(xù)的。這在后續(xù)的使用過程中,就可以盡可能的最大化利用所釋放出來的空間。這個(gè)過程就是標(biāo)記整理算法,會(huì)配合著標(biāo)記清除,在V8引擎中實(shí)現(xiàn)頻繁的GC操作。

8. 執(zhí)行時(shí)機(jī)

首先是引用計(jì)數(shù),他的可以及時(shí)回收垃圾對(duì)象,只要數(shù)值0的就會(huì)立即讓GC找到這片空間進(jìn)行回收和釋放。正是由于這個(gè)特點(diǎn)的存在,引用計(jì)數(shù)可以最大限度的減少程序的卡頓,因?yàn)橹灰@個(gè)空間即將被占滿的時(shí)候,垃圾回收器就會(huì)進(jìn)行工作,將內(nèi)存進(jìn)行釋放,讓內(nèi)存空間總有一些可用的地方。

標(biāo)記清除不能立即回收垃圾對(duì)象,而且他去清除的時(shí)候當(dāng)前的程序其實(shí)是停止工作的。即便第一階段發(fā)現(xiàn)了垃圾,也要等到第二階段清除的時(shí)候才會(huì)回收掉。

標(biāo)記整理也不能立即回收垃圾對(duì)象。

9. V8引擎

眾所周知V8引擎是目前市面上最主流的JavaScript執(zhí)行引擎,日常所使用的chrome瀏覽器以及NodeJavaScript平臺(tái)都在采用這個(gè)引擎去執(zhí)行JavaScript代碼。對(duì)于這兩個(gè)平臺(tái)來看JavaScript之所以能高效的運(yùn)轉(zhuǎn),也正是因?yàn)閂8的存在。V8的速度之所以快,除了有一套優(yōu)秀的內(nèi)存管理機(jī)制之外,還有一個(gè)特點(diǎn)就是采用及時(shí)編譯。

之前很多的JavaScript引擎都需要將源代碼轉(zhuǎn)成字節(jié)碼才能執(zhí)行,而V8可以將源碼翻譯成直接執(zhí)行的機(jī)器碼。所以執(zhí)行速度是非常快的。

V8還有一個(gè)比較大的特點(diǎn)就是他的內(nèi)存是有上限的,在64位操作系統(tǒng)下,上限是不超過1.5G,在32位的操作系統(tǒng)中數(shù)值是不超過800M。

為什么V8要采用這樣的做法呢,原因基本上可以從兩方面進(jìn)行說明。

第一V8本身就是為了瀏覽器制造的,所以現(xiàn)有的內(nèi)存大小足夠使用了。再有V8內(nèi)部所實(shí)現(xiàn)的垃圾回收機(jī)制也決定了他采用這樣一個(gè)設(shè)置是非常合理的。因?yàn)楣俜阶鲞^一個(gè)測(cè)試,當(dāng)垃圾內(nèi)存達(dá)到1.5個(gè)G的時(shí)候,V8去采用增量標(biāo)記的算法進(jìn)行垃圾回收只需要消耗50ms,采用非增量標(biāo)記的形式回收則需要1s。從用戶體驗(yàn)的角度來說1s已經(jīng)算是很長(zhǎng)的時(shí)間了,所以就以1.5G為界了。

1. 垃圾回收策略

在程序的使用過程中會(huì)用到很多的數(shù)據(jù),數(shù)據(jù)又可以分為原始的數(shù)據(jù)和對(duì)象類型的數(shù)據(jù)?;A(chǔ)的原始數(shù)據(jù)都是由程序的語言自身來進(jìn)行控制的。所以這里所提到的回收主要還是指的是存活在堆區(qū)里的對(duì)象數(shù)據(jù),因此這個(gè)過程是離不開內(nèi)存操作的。

V8采用的是分代回收的思想,把內(nèi)存空間按照一定的規(guī)則分成兩類,新生代存儲(chǔ)區(qū)和老生代存儲(chǔ)區(qū)。有了分類后,就會(huì)針對(duì)不同代采用最高效的GC算法,從而對(duì)不同的對(duì)象進(jìn)行回收操作。這也就意味著V8回收會(huì)使用到很多的GC算法。

首先,分代回收算法肯定是要用到的,因?yàn)樗仨氁龇执?。緊接著會(huì)用到空間的復(fù)制算法。除此以外還會(huì)用到標(biāo)記清除和標(biāo)記整理。最后為了去提高效率,又用到了標(biāo)記增量。

2. 回收新生代對(duì)象

首先是要說明一下V8內(nèi)部的內(nèi)存分配。因?yàn)樗腔诜执睦厥账枷?,所以在V8內(nèi)部是把內(nèi)存空間分成了兩個(gè)部分,可以理解成一個(gè)存儲(chǔ)區(qū)域被分成了左右兩個(gè)區(qū)域。左側(cè)的空間是專門用來存放新生代對(duì)象,右側(cè)專門存放老生代對(duì)象。新生代對(duì)象空間是有一定設(shè)置的,在64位操作系統(tǒng)中大小是32M,在32位的操作系統(tǒng)中是16M。

新生代對(duì)象其實(shí)指的就是存活時(shí)間較短的。比如說當(dāng)前代碼內(nèi)有個(gè)局部的作用域,作用域中的變量在執(zhí)行完成過后就要被回收,在其他地方比如全局也有一個(gè)變量,而全局的變量肯定要等到程序退出之后才會(huì)被回收。所以相對(duì)來說新生代就指的是那些存活時(shí)間比較短的那樣一些變量對(duì)象。

針對(duì)新生代對(duì)象回收所采用到的算法主要是復(fù)制算法和標(biāo)記整理算法,首先會(huì)將左側(cè)一部分小空間也分成兩個(gè)部分,叫做From和To,而且這兩個(gè)部分的大小是相等的,將From空間稱為使用狀態(tài),To空間叫做空閑狀態(tài)。有了這樣兩個(gè)空間之后代碼執(zhí)行的時(shí)候如果需要申請(qǐng)空間首先會(huì)將所有的變量對(duì)象都分配至From空間。也就是說在這個(gè)過程中To是空閑的,一旦From空間應(yīng)用到一定的程度之后,就要觸發(fā)GC操作。這個(gè)時(shí)候就會(huì)采用標(biāo)記整理對(duì)From空間進(jìn)行標(biāo)記,找到活動(dòng)對(duì)象,然后使用整理操作把他們的位置變得連續(xù),便于后續(xù)不會(huì)產(chǎn)生碎片化空間。

做完這些操作以后,將活動(dòng)對(duì)象拷貝至To空間,也就意味著From空間中的活動(dòng)對(duì)象有了一個(gè)備份,這時(shí)候就可以考慮回收了。回收也非常簡(jiǎn)單,只需要把From空間完全釋放就可以了,這個(gè)過程也就完成了新生代對(duì)象的回收操作。

總結(jié)一下就是新生代對(duì)象的存儲(chǔ)區(qū)域被一分為二,而且是兩個(gè)等大的,在這兩個(gè)等大的空間中,起名From和To,當(dāng)前使用的是From,所有的對(duì)象聲明都會(huì)放在這個(gè)空間內(nèi)。觸發(fā)GC機(jī)制的時(shí)候會(huì)把活動(dòng)對(duì)象全部找到進(jìn)行整理,拷貝到To空間中。拷貝完成以后我們讓From和To進(jìn)行空間交換(也就是名字的交換),原來的To就變成了From,原來的From就變成了To。這樣就算完成了空間的釋放和回收。

接下來針對(duì)過程的細(xì)節(jié)進(jìn)行說明。首先在這個(gè)過程中肯定會(huì)想到的是,如果在拷貝時(shí)發(fā)現(xiàn)某一個(gè)變量對(duì)象所指的空間,在當(dāng)前的老生代對(duì)象里面也會(huì)出現(xiàn)。這個(gè)時(shí)候就會(huì)出現(xiàn)一個(gè)所謂的叫晉升的操作,就是將新生代的對(duì)象,移動(dòng)至老生代進(jìn)行存儲(chǔ)。

至于什么時(shí)候觸發(fā)晉升操作一般有兩個(gè)判斷標(biāo)準(zhǔn),第一個(gè)是如果新生代中的某些對(duì)象經(jīng)過一輪GC之后他還活著。這個(gè)時(shí)候就可以把他拷貝至老年代存儲(chǔ)區(qū),進(jìn)行存儲(chǔ)。除此之外如果當(dāng)前拷貝的過程中,發(fā)現(xiàn)To空間的使用率超過了25%,這個(gè)時(shí)候也需要將這一次的活動(dòng)對(duì)象都移動(dòng)至老生代中存放。

為什么要選擇25%呢?其實(shí)也很容易想得通,因?yàn)閷磉M(jìn)行回收操作的時(shí)候,最終是要把From空間和To空間進(jìn)行交換的。也就是說以前的To會(huì)變成From,而以前的From要變成To,這就意味著To如果使用率達(dá)到了80%,最終變成活動(dòng)對(duì)象的存儲(chǔ)空間后,新的對(duì)象好像存不進(jìn)去了。簡(jiǎn)單的說明就是To空間的使用率如果超過了一定的限制,將來變成使用狀態(tài)時(shí),新進(jìn)來的對(duì)象空間好像不那么夠用,所以會(huì)有這樣的限制。

簡(jiǎn)單總結(jié)一下就是當(dāng)前內(nèi)存一分為二,一部分用來存儲(chǔ)新生代對(duì)象,至于什么是新生代對(duì)象可以認(rèn)為他的存活時(shí)間相對(duì)較短。然后可以去采用標(biāo)記整理的算法,對(duì)From空間進(jìn)行活動(dòng)對(duì)象的標(biāo)記和整理操作,接著把他們拷貝To空間。最后再置換一下兩個(gè)空間的狀態(tài),那此時(shí)也就完成了空間的釋放操作。

3. 回收老生代對(duì)象

老生代對(duì)象存放在內(nèi)存空間的右側(cè),在V8中同樣是有內(nèi)存大小的限制,在64位操作系統(tǒng)中大小是1.4G, 在32位操作系統(tǒng)中是700M。

老生代對(duì)象指的是存活時(shí)間較長(zhǎng)的對(duì)象,例如之前所提到的在全局對(duì)象中存放的一些變量,或者是一些閉包里面放置的變量有可能也會(huì)存活很長(zhǎng)的時(shí)間。針對(duì)老生代垃圾回收主要采用的是標(biāo)記清除,標(biāo)記整理和增量標(biāo)記三個(gè)算法。

使用時(shí)主要采用的是標(biāo)記清除算法完成垃圾空間的釋放和回收,標(biāo)記清除算法主要是找到老生代存儲(chǔ)區(qū)域中的所有活動(dòng)對(duì)象進(jìn)行標(biāo)記,然后直接釋放掉那些垃圾數(shù)據(jù)空間就可以了。顯而易見這個(gè)地方會(huì)存在一些空間碎片化的問題,不過雖然有這樣的問題但是V8的底層主要使用的還是標(biāo)記清除的算法。因?yàn)橄鄬?duì)空間碎片來說他的提升速度是非常明顯的。

在什么情況下會(huì)使用到標(biāo)記整理算法呢?當(dāng)需要把新生代里的內(nèi)容向老生代中移動(dòng)的時(shí)候,而且這個(gè)時(shí)間節(jié)點(diǎn)上老生代存儲(chǔ)區(qū)域的空間又不足以存放新生代存儲(chǔ)區(qū)移過來的對(duì)象。這種情況下就會(huì)觸發(fā)標(biāo)記整理,把之前的一些鎖片空間進(jìn)行整理回收,讓程序有更多的空間可以使用。最后還會(huì)采用增量標(biāo)記的方式對(duì)回收的效率進(jìn)行提升。

這里來對(duì)比一下新老生代垃圾回收。

新生代的垃圾回收更像是在用空間換時(shí)間,因?yàn)樗捎玫氖菑?fù)制算法,這也就意味著每時(shí)每刻他的內(nèi)部都會(huì)有一個(gè)空閑空間的存在。但是由于新生代存儲(chǔ)區(qū)本身的空間很小,所以分出來的空間更小,這部分的空間浪費(fèi)相比帶來的時(shí)間上的一個(gè)提升當(dāng)然是微不足道的。

在老生代對(duì)象回收過程中為什么不去采用這種一分二位的做法呢?因?yàn)槔仙鎯?chǔ)空間是比較大的,如果一分為二就有幾百兆的空間浪費(fèi),太奢侈了。第二就是老生代存儲(chǔ)區(qū)域中所存放的對(duì)象數(shù)據(jù)比較多,所以在賦值的過程中消耗的時(shí)間也就非常多,因此老生代的垃圾回收是不適合使用復(fù)制算法來實(shí)現(xiàn)的。

至于之前所提到的增量標(biāo)記算法是如何優(yōu)化垃圾回收操作的呢?首先分成兩個(gè)部分,一個(gè)是程序執(zhí)行,另一個(gè)是垃圾回收。

首先明確垃圾回收進(jìn)行工作的時(shí)候是會(huì)阻塞當(dāng)前JavaScript程序執(zhí)行的,也就是會(huì)出現(xiàn)一個(gè)空檔期,例如程序執(zhí)行完成之后會(huì)停下來執(zhí)行垃圾回收操作。所謂的標(biāo)記增量簡(jiǎn)單來講就是將整段的垃圾回收操作拆分成多個(gè)小步驟,組分片完成整個(gè)回收,替代之前一口氣做完的垃圾回收操作。

這樣做的好處主要是實(shí)現(xiàn)垃圾回收與程序執(zhí)行交替完成,帶來的時(shí)間消耗會(huì)更加的合理一些。避免像以前那樣程序執(zhí)行的時(shí)候不能做垃圾回收,程序做垃圾回收的時(shí)候不能繼續(xù)運(yùn)行程序。

簡(jiǎn)單的舉個(gè)例子說明一下增量標(biāo)記的實(shí)現(xiàn)原理。

程序首先運(yùn)行的時(shí)候是不需要進(jìn)行垃圾回收的,一旦當(dāng)他觸發(fā)了垃圾回收之后,無論采用的是何種算法,都會(huì)進(jìn)行遍歷和標(biāo)記操作,這里針對(duì)的是老生代存儲(chǔ)區(qū)域,所以存在遍歷操作。在遍歷的過程中需要做標(biāo)記,標(biāo)記之前也提到過可以不一口氣做完,因?yàn)榇嬖谥苯涌蛇_(dá)和間接可達(dá)操作,也就是說如果在做的時(shí)候,第一步先找到第一層的可達(dá)對(duì)象。然后就可以停下來,讓程序再去執(zhí)行一會(huì)。如果說程序執(zhí)行了一會(huì)以后,再繼續(xù)讓GC機(jī)做第二步的標(biāo)記操作,比如下面還有一些子元素也是可達(dá)的,那就繼續(xù)做標(biāo)記。標(biāo)記一輪之后再讓GC停下來,繼續(xù)回到程序執(zhí)行,也就是交替的去做標(biāo)記和程序執(zhí)行。

最后標(biāo)記操作完成以后再去完成垃圾回收,這段時(shí)間程序就要停下來,等到垃圾回收操作完成才會(huì)繼續(xù)執(zhí)行。雖然這樣看起來程序停頓了很多次,但是整個(gè)V8最大的垃圾回收也就是當(dāng)內(nèi)存達(dá)到1.5G的時(shí)候,采用非增量標(biāo)記的形式進(jìn)行垃圾回收時(shí)間也不超過1s,所以這里程序的間斷是合理的。而且這樣一來最大限度的把以前很長(zhǎng)的一段停頓時(shí)間直接拆分成了更小段,針對(duì)用戶體驗(yàn)會(huì)顯得更加流程一些。

4. V8垃圾回收總結(jié)

首先要知道V8引擎是當(dāng)前主流的JavaScript執(zhí)行引擎,在V8的內(nèi)部?jī)?nèi)存是設(shè)置上限的,這么做的原因是第一他本身是為瀏覽器而設(shè)置的,所以在web應(yīng)用中這樣的內(nèi)存大小是足夠使用的。第二就是由他內(nèi)部的垃圾回收機(jī)制來決定的,如果把內(nèi)存設(shè)置大一些這個(gè)時(shí)候回收時(shí)間最多可能就超過了用戶的感知,所以這里就設(shè)置了上限數(shù)值。

V8采用的是分代回收的思想,將內(nèi)存分成了新生代和老生代。關(guān)于新生代和老生代在空間和存儲(chǔ)數(shù)據(jù)類型是不同的。新生代如果在64位操作系統(tǒng)下空間是32M,32位的系統(tǒng)下就是16M。

V8對(duì)不同代對(duì)象采用的是不同的GC算法來完成垃圾回收操作,具體就是針對(duì)新生代采用復(fù)制算法和標(biāo)記整理算法,針對(duì)老生代對(duì)象主要采用標(biāo)記清除,標(biāo)記整理和增量標(biāo)記這樣三個(gè)算法。

10. Performance工具介紹

GC工作目的就是為了讓內(nèi)存空間在程序運(yùn)行的過程中,出現(xiàn)良性的循環(huán)使用。所謂良性循環(huán)的基礎(chǔ)其實(shí)就是要求開發(fā)者在寫代碼的時(shí)候能夠?qū)?nèi)存空間進(jìn)行合理的分配。但是由于ECMAScript中并沒有給程序員提供相應(yīng)的操作內(nèi)存空間的API,所以是否合理好像也不知道,因?yàn)樗际怯蒅C自動(dòng)完成的。

如果想判斷整個(gè)過程內(nèi)存使用是否合理,必須想辦法能夠時(shí)刻關(guān)注到內(nèi)存的變化。所以就有了這樣一款工具可以提供給開發(fā)者更多的監(jiān)控方式,在程序運(yùn)行過程中幫助開發(fā)者完成對(duì)內(nèi)存空間的監(jiān)控。

通過使用Performance可以對(duì)程序運(yùn)行過程內(nèi)存的變化實(shí)時(shí)的監(jiān)控。這樣就可以在程序的內(nèi)存出現(xiàn)問題的時(shí)候直接想辦法定位到出現(xiàn)問題的代碼快。下面來看一下Performance工具的基本使用步驟。

首先打開瀏覽器,在地址欄輸入網(wǎng)址。輸入完地址之后不建議立即進(jìn)行訪問,因?yàn)橄氚炎畛醯匿秩具^程記錄下來,所以只是打開界面輸入網(wǎng)址即可。緊接著打開開發(fā)人員工具面板(F12),選擇性能選項(xiàng)。開啟錄制功能,開啟之后就可以訪問目標(biāo)網(wǎng)址了。在這個(gè)頁面上進(jìn)行一些操作,過一段時(shí)間后停止錄制。

就可以得到一個(gè)報(bào)告,在報(bào)告當(dāng)中就可以分析跟內(nèi)存相關(guān)的信息了。錄制后會(huì)有一些圖表的展示,信息也非常的多,看起來比較麻煩。這里主要關(guān)注與內(nèi)存相關(guān)的信息,有一個(gè)內(nèi)存的選項(xiàng)(Memory)。默認(rèn)情況下如果沒有勾選需要將它勾選。頁面上可以看到一個(gè)藍(lán)色的線條。屬于整個(gè)過程中我內(nèi)存所發(fā)生的變化,可以根據(jù)時(shí)序,來看有問題的地方。如果某個(gè)地方有問題可以具體觀察,比如有升有降就是沒問題的。

1. 內(nèi)存問題的體現(xiàn)

當(dāng)程序的內(nèi)存出現(xiàn)問題的時(shí)候,具體會(huì)表現(xiàn)出什么樣的形式。

首先第一條,界面如果出現(xiàn)了延遲加載或者說經(jīng)常性的暫停,首先限定一下網(wǎng)絡(luò)環(huán)境肯定是正常的,所以出現(xiàn)這種情況一般都會(huì)去判定內(nèi)存是有問題的,而且與GC存在著頻繁的垃圾回收操作是相關(guān)的。也就是代碼中肯定存在瞬間讓內(nèi)存爆炸的代碼。這樣的代碼是不合適的需要去進(jìn)行定位。

第二個(gè)就是當(dāng)界面出現(xiàn)了持續(xù)性的糟糕性能表現(xiàn),也就是說在使用過程中,一直都不是特別的好用,這種情況底層一般會(huì)認(rèn)為存在著內(nèi)存膨脹。所謂的內(nèi)存膨脹指的就是,當(dāng)前界面為了達(dá)到最佳的使用速度,可能會(huì)申請(qǐng)一定的內(nèi)存空間,但是這個(gè)內(nèi)存空間的大小,遠(yuǎn)超過了當(dāng)前設(shè)備本身所能提供的大小,這個(gè)時(shí)候就會(huì)感知到一段持續(xù)性的糟糕性能的體驗(yàn),同樣肯定是假設(shè)當(dāng)前網(wǎng)絡(luò)環(huán)境是正常的。

最后,當(dāng)使用一些界面的時(shí)候,如果感知到界面的使用流暢度,隨著時(shí)間的加長(zhǎng)越來越慢,或者說越來越差,這個(gè)過程就伴隨著內(nèi)存泄露,因?yàn)樵谶@種情況下剛開始的時(shí)候是沒有問題的,由于我們某些代碼的出現(xiàn),可能隨著時(shí)間的增長(zhǎng)讓內(nèi)存空間越來越少,這也就是所謂的內(nèi)存泄漏,因此,出現(xiàn)這種情況的時(shí)候界面會(huì)隨著使用時(shí)間的增長(zhǎng)表現(xiàn)出性能越來越差的現(xiàn)象。

這就是關(guān)于應(yīng)用程序在執(zhí)行過程中如果遇到了內(nèi)存出現(xiàn)問題的情況,具體的體現(xiàn)可以結(jié)合Performance進(jìn)行內(nèi)存分析操作,從而定位到有問題的代碼,修改之后讓應(yīng)用程序在執(zhí)行的過程中顯得更加流暢。

2. 監(jiān)控內(nèi)存的幾種方式

內(nèi)存出現(xiàn)的問題一般歸納為三種:內(nèi)存泄露,內(nèi)存膨脹,頻繁的垃圾回收。當(dāng)這些內(nèi)容出現(xiàn)的時(shí)候,該以什么樣的標(biāo)準(zhǔn)來進(jìn)行界定呢?

內(nèi)存泄露其實(shí)就是內(nèi)存持續(xù)升高,這個(gè)很好判斷,當(dāng)前已經(jīng)有很多種方式可以獲取到應(yīng)用程序執(zhí)行過程中內(nèi)存的走勢(shì)圖。如果發(fā)現(xiàn)內(nèi)存一直持續(xù)升高的,整個(gè)過程沒有下降的節(jié)點(diǎn),這也就意味著程序代碼中是存在內(nèi)存泄露的。這個(gè)時(shí)候應(yīng)該去代碼里面定位相應(yīng)的模塊。

內(nèi)存膨脹相對(duì)的模糊,內(nèi)存膨脹的本意指的是應(yīng)用程序本身,為了達(dá)到最優(yōu)的效果,需要很大的內(nèi)存空間,在這個(gè)過程中也許是由于當(dāng)前設(shè)備本身的硬件不支持,才造成了使用過程中出現(xiàn)了一些性能上的差異。想要判定是程序問題還是設(shè)備問題,應(yīng)該多做一些測(cè)試。這個(gè)時(shí)候可以找到那些深受用戶喜愛的設(shè)備,在他們上面運(yùn)行應(yīng)用程序,如果整個(gè)過程中所有的設(shè)備都表現(xiàn)出了很糟糕的性能體驗(yàn)。這就說明程序本身是有問題的,而不是設(shè)備有問題。這種情況就需要回到代碼里面,定位到內(nèi)存出現(xiàn)問題的地方。

具體有哪些方式來監(jiān)控內(nèi)存的變化,主要還是采用瀏覽器所提供的一些工具。

瀏覽器所帶的任務(wù)管理器,可以直接以數(shù)值的方式將當(dāng)前應(yīng)用程序在執(zhí)行過程中內(nèi)存的變化體現(xiàn)出來。第二個(gè)是借助于Timeline時(shí)序圖,直接把應(yīng)用程序執(zhí)行過程中所有內(nèi)存的走勢(shì)以時(shí)間點(diǎn)的方式呈現(xiàn)出來,有了這張圖就可以很容易的做判斷了。再有瀏覽器中還會(huì)有一個(gè)叫做堆快照的功能,可以很有針對(duì)性的查找界面對(duì)象中是否存在一些分離的DOM,因?yàn)榉蛛xDOM的存在也就是一種內(nèi)存上的泄露。

至于怎樣判斷界面是否存在著頻繁的垃圾回收,這就需要借助于不同的工具來獲取當(dāng)前內(nèi)存的走勢(shì)圖,然后進(jìn)行一個(gè)時(shí)間段的分析,從而得出判斷。

3. 任務(wù)管理器監(jiān)控內(nèi)存

一個(gè)web應(yīng)用在執(zhí)行的過程中,如果想要觀察他內(nèi)部的一個(gè)內(nèi)存變化,是可以有多種方式的,這里通過一段簡(jiǎn)單的demo來演示一下,可以借助瀏覽器中自帶的任務(wù)管理器監(jiān)控腳本運(yùn)行時(shí)內(nèi)存的變化。

在界面中放置一個(gè)元素,添加一個(gè)點(diǎn)擊事件,事件觸發(fā)的時(shí)候創(chuàng)建一個(gè)長(zhǎng)度非常長(zhǎng)的一個(gè)數(shù)組。這樣就會(huì)產(chǎn)生內(nèi)存空間上的消耗。

<body>
    <button id="btn">add</button>
    <script>
        const oBtn = document.getElementById('btn');
        oBtn.onclick = function() {
            let arrList = new Array(1000000)
        }
    </script>
</body>

完成之后打開瀏覽器運(yùn)行,在右上角的更多中找到更多工具找到任務(wù)管理器打開。

這個(gè)時(shí)候就可以在任務(wù)管理器中定位到當(dāng)前正在執(zhí)行的腳本,默認(rèn)情況下是沒有JavaScript內(nèi)存列的,如果需要可以直接右擊找到JavaScript內(nèi)存展示出來。這里最關(guān)注的是內(nèi)存和JavaScript內(nèi)存這兩列。

第一列內(nèi)存表示的是原生內(nèi)存,也就是當(dāng)前界面會(huì)有很多DOM節(jié)點(diǎn),這個(gè)內(nèi)存指的就是DOM節(jié)點(diǎn)所占據(jù)的內(nèi)存,如果這個(gè)數(shù)值在持續(xù)的增大,就說明界面中在不斷的創(chuàng)建DOM元素。

JavaScript內(nèi)存表示的是JavaScript的堆,在這列當(dāng)中需要關(guān)注的是小括號(hào)里面的值,表示的是界面中所有可達(dá)對(duì)象正在使用的內(nèi)存大小,如果這個(gè)數(shù)值一直在增大,就意味著當(dāng)前的界面中要么在創(chuàng)建新對(duì)象,要么就是現(xiàn)有對(duì)象在不斷的增長(zhǎng)。

以這個(gè)界面為例,可以發(fā)現(xiàn)小括號(hào)的值一直是個(gè)穩(wěn)定的數(shù)字沒有發(fā)生變化,也就意味著當(dāng)前頁面是沒有內(nèi)存增長(zhǎng)的。此時(shí)可以再去觸發(fā)一下click事件(點(diǎn)擊按鈕),多點(diǎn)幾次,完成以后就發(fā)現(xiàn)小括號(hào)里面的數(shù)值變大了。

通過這樣的過程就可以借助當(dāng)前的瀏覽器任務(wù)管理器來監(jiān)控腳本運(yùn)行時(shí)整個(gè)內(nèi)存的變化。如果當(dāng)前JavaScript內(nèi)存列小括號(hào)里面的數(shù)值一直增大那就意味著內(nèi)存是有問題的,當(dāng)然這個(gè)工具是沒有辦法定位的,他只能發(fā)現(xiàn)問題,無法定位問題。

4. TimeLine記錄內(nèi)容

在之前已經(jīng)可以使用瀏覽器自帶的任務(wù)管理器對(duì)腳本執(zhí)行中內(nèi)存的變化去進(jìn)行監(jiān)控,但是在使用的過程中可以發(fā)現(xiàn),這樣的操作更多的是用于判斷當(dāng)前腳本的內(nèi)存是否存在問題。如果想要定位問題具體和什么樣的腳本有關(guān),任務(wù)管理器就不是那么好用了。

這里再介紹一個(gè)通過時(shí)間線記錄內(nèi)存變化的方式來演示一下怎樣更精確的定位到內(nèi)存的問題跟哪一塊代碼相關(guān),或者在什么時(shí)間節(jié)點(diǎn)上發(fā)生的。

首先放置一個(gè)DOM節(jié)點(diǎn),添加點(diǎn)擊事件,在事件中創(chuàng)建大量的DOM節(jié)點(diǎn)來模擬內(nèi)存消耗,再通過數(shù)組的方式配合著其他的方法形成一個(gè)非常長(zhǎng)的字符串,模擬大量的內(nèi)存消耗。

<body>
    <button id="btn">add</button>
    <script>
        const oBtn = document.getElementById('btn');

        const arrList = [];

        function test () {
            for (let i = 0; i < 100000; i++) {
                document.body.appendChild(document.createElement('p'))
            }
            arrList.push(new Array(1000000).join('x'))
        }
        oBtn.onclick = test;
    </script>
</body>

先打開瀏覽器的控制臺(tái)工具,選擇性能面板,默認(rèn)是沒有運(yùn)行的,也就是沒有記錄,需要先點(diǎn)擊計(jì)時(shí)操作。點(diǎn)完以后就開始錄制了,點(diǎn)擊幾次add按鈕,稍等幾秒后,點(diǎn)擊停止按鈕。完成以后就生成了一個(gè)圖表,密密麻麻的東西看起來可能會(huì)有些頭疼,只關(guān)注下想要看到的信息就可以了。

內(nèi)存如果沒有勾選的話是不會(huì)監(jiān)控內(nèi)存變化的,需要先勾選內(nèi)存,勾選之后頁面上就出現(xiàn)了內(nèi)存的走勢(shì)曲線圖。里面會(huì)包含很多信息,給出來了幾中顏色的解釋。藍(lán)色的是JavaScript堆,紅色表示當(dāng)前的文檔,綠色是DOM節(jié)點(diǎn),棕色是監(jiān)聽器,紫色是CPU內(nèi)存。

為了便于觀察可以只保留JavaScript堆,其他的取消勾選隱藏掉??梢钥吹竭@個(gè)腳本運(yùn)行過程中到目前為止他的JavaScript堆的情況走勢(shì)。當(dāng)前這個(gè)工具叫時(shí)序圖,也就是在第一欄,以毫秒為單位,記錄了整個(gè)頁面從空白到渲染結(jié)束到最終停狀態(tài),這個(gè)過程中整個(gè)界面的變化。如果愿意,可以點(diǎn)進(jìn)去看一下當(dāng)前的界面形態(tài),如果只是關(guān)注內(nèi)存,只看內(nèi)存的曲線圖就可以了。

當(dāng)這個(gè)頁面最開始打開的時(shí)候其實(shí)很長(zhǎng)一段時(shí)間都是平穩(wěn)的狀態(tài),沒有太多的內(nèi)存消耗。原因在根本沒有點(diǎn)擊add。然后緊接著在某一個(gè)時(shí)間點(diǎn)上突然之間內(nèi)存就上去了,上去之后是一段平穩(wěn)的狀態(tài),這是因?yàn)辄c(diǎn)擊了add之后這里的內(nèi)存肯定是瞬間暴漲的,然后緊接著暴漲之后我們?nèi)魏尾僮?,所以這時(shí)候肯定是平穩(wěn)。

然后緊接著平穩(wěn)之后又下降了,這就是之前所提到的,瀏覽器本身也是具有垃圾回收機(jī)制的,當(dāng)?shù)哪_本運(yùn)行穩(wěn)定之后,GC可能在某個(gè)時(shí)間點(diǎn)上就開始工作了,會(huì)發(fā)現(xiàn)有一些對(duì)象是非活動(dòng)的,就開始進(jìn)行回收,所以一段平穩(wěn)之后就降下去了。降下去之后又會(huì)有一些小的浮動(dòng),屬于正常的活動(dòng)開銷。后來又有幾次連續(xù)的點(diǎn)擊,這個(gè)連續(xù)的點(diǎn)擊行為可能又造成內(nèi)存的飆升,然后不操作之后又往下降。

通過這樣一張內(nèi)存走勢(shì)圖,可以得出的結(jié)論是,腳本里面內(nèi)存是非常穩(wěn)定的,整個(gè)過程有漲有降,漲是申請(qǐng)內(nèi)存,降是用完之后我GC在正常的回收內(nèi)存。

一旦看到內(nèi)存的走勢(shì)是直線向上走,也就意味著他只有增長(zhǎng)而沒有回收,必然存在著內(nèi)存消耗,更有可能是內(nèi)存泄漏??梢酝ㄟ^上面的時(shí)序圖定位問題,當(dāng)發(fā)現(xiàn)某一個(gè)節(jié)點(diǎn)上有問題的時(shí)候,可以直接在這里面定位到那個(gè)時(shí)間節(jié)點(diǎn),可以在時(shí)序圖上進(jìn)行拖動(dòng)查看每一個(gè)時(shí)間節(jié)點(diǎn)上的內(nèi)存消耗。還可以看到界面上的變化,就可以配合著定位到是哪一塊產(chǎn)生了這樣一個(gè)內(nèi)存的問題。

所以相對(duì)任務(wù)管理器來說會(huì)更好用,不但可以看當(dāng)前內(nèi)存是否有問題,還可以幫助定位問題在哪個(gè)時(shí)候發(fā)生的,然后再配合當(dāng)前的界面展示知道做了什么樣的操作才出現(xiàn)了這個(gè)問題,從而間接地可以回到代碼中定位有問題的代碼塊。

5. 堆快照查找分離DOM

這里簡(jiǎn)單說明一下堆快照功能工作的原理,首先他相當(dāng)于找到JavaScript堆,然后對(duì)它進(jìn)行照片的留存。有了照片以后就可以看到它里面的所有信息,這也就是監(jiān)控的由來。堆快照在使用的時(shí)候非常的有用,因?yàn)樗袷轻槍?duì)分離DOM的查找行為。

界面上看到的很多元素其實(shí)都是DOM節(jié)點(diǎn),而這些DOM節(jié)點(diǎn)本應(yīng)該存在于一顆存活的DOM樹上。不過DOM節(jié)點(diǎn)會(huì)有幾種形態(tài),一種是垃圾對(duì)象,一種是分離DOM。簡(jiǎn)單的說就是如果這個(gè)節(jié)點(diǎn)從DOM樹上進(jìn)行了脫離,而且在JavaScript代碼當(dāng)中沒有再引用的DOM節(jié)點(diǎn),他就成為了一個(gè)垃圾。如果DOM節(jié)點(diǎn)只是從DOM樹上脫離了,但是在JavaScript代碼中還有引用,就是分離DOM。分離DOM在界面上是看不見的,但是在內(nèi)存中是占據(jù)著空間的。

這種情況就是一種內(nèi)存泄露,可以通過堆快照的功能把他們找出來,只要能找得到,就可以回到代碼里,針對(duì)這些代碼進(jìn)行清除從而讓內(nèi)存得到一些釋放,腳本在執(zhí)行的時(shí)候也會(huì)變得更加迅速。

在html里面放入btn按鈕,添加點(diǎn)擊事件,點(diǎn)擊按鈕的時(shí)候,通過JavaScript語句去模擬相應(yīng)的內(nèi)存變化,比如創(chuàng)建DOM節(jié)點(diǎn),為了看到更多類型的分離DOM,采用ul包裹li的DOM節(jié)點(diǎn)創(chuàng)建。先在函數(shù)中創(chuàng)建ul節(jié)點(diǎn),然后使用循環(huán)的方式創(chuàng)建多個(gè)li放在ul里面,創(chuàng)建之后不需要放在頁面上,為了讓代碼引用到這個(gè)DOM使用變量tmpEle指向ul。

<body>
    <button id="btn">add</button>
    <script>
        const oBtn = document.getElementById('btn');

        var tmpEle;

        function fn () {
            var ul = document.createElement('ul');
            for (var i = 0; i < 10; i++) {
                var li = document.createElement('li');
                ul.appendChild(li);
            }
            tmpEle = ul;
        }

        oBtn.addEventListener('click', fn);

    </script>
</body>

簡(jiǎn)單說明就是創(chuàng)建了ul和li節(jié)點(diǎn),但是并沒有將他們放在頁面中,只是通過JavaScript變量引用了這個(gè)節(jié)點(diǎn),這就是分離DOM。

打開瀏覽器調(diào)試工具,選中內(nèi)存面板。進(jìn)入以后可以發(fā)現(xiàn)堆快照的選項(xiàng)。這里做兩個(gè)行為的測(cè)試,第一個(gè)是在沒有點(diǎn)擊按鈕的情況下,直接獲取當(dāng)前的快照,在這個(gè)快照里面就是當(dāng)前對(duì)象的具體展示,這里有一個(gè)篩選的操作,直接檢索deta關(guān)鍵字,可以發(fā)現(xiàn)沒有內(nèi)容。

回到界面中做另外一個(gè)操作,對(duì)按鈕進(jìn)行點(diǎn)擊,點(diǎn)完以后我再拍攝一張快照(點(diǎn)擊左側(cè)的配置文件文字,出現(xiàn)拍照界面),還是做和之前一樣的操作檢索deta。

這次就會(huì)發(fā)現(xiàn),快照2里面搜索到了,很明顯這幾個(gè)就是代碼中所創(chuàng)建的DOM節(jié)點(diǎn),并沒有添加到界面中,但是他的確存在于堆中。這其實(shí)就是一種空間上的浪費(fèi),針對(duì)這樣的問題在代碼中對(duì)使用過后的DOM節(jié)點(diǎn)進(jìn)行清空就可以了。

function fn () {
    var ul = document.createElement('ul');
    for (var i = 0; i < 10; i++) {
        var li = document.createElement('li');
        ul.appendChild(li);
    }
    tmpEle = ul;
    // 清空DOM
    ul = null;
}

在這里我們簡(jiǎn)單的總結(jié)就是,我們可以利用瀏覽器當(dāng)中提供的一個(gè)叫做堆快照的功能,然后去把我們當(dāng)前的堆進(jìn)行拍照,拍照過后我們要找一下這里面是否存在所謂的分離DOM。

因?yàn)榉蛛xDOM在頁面中不體現(xiàn),在內(nèi)存中的確存在,所以這個(gè)時(shí)候他是一種內(nèi)存的浪費(fèi),那么我們要做的就是定位到我們代碼里面那些個(gè)分離DOM所在的位置,然后去想辦法把他給清除掉。

6. 判斷是否存在頻繁GC

這里說一下如何確定當(dāng)前web應(yīng)用在執(zhí)行過程中是否存在著頻繁的垃圾回收。當(dāng)GC去工作的時(shí)候應(yīng)用程序是停止的。所以GC頻繁的工作對(duì)web應(yīng)用很不友好,因?yàn)闀?huì)處于死的狀態(tài),用戶會(huì)感覺到卡頓。

這個(gè)時(shí)候就要想辦法確定當(dāng)前的應(yīng)用在執(zhí)行時(shí)是否存在頻繁的垃圾回收。

這里給出兩種方式,第一種是可以通過timeline時(shí)序圖的走勢(shì)來判斷,在性能工具面板中對(duì)當(dāng)前的內(nèi)存走勢(shì)進(jìn)行監(jiān)控。如果發(fā)現(xiàn)藍(lán)色的走勢(shì)條頻繁的上升下降。就意味著在頻繁的進(jìn)行垃圾回收。出現(xiàn)這樣的情況之后必須定位到相應(yīng)的時(shí)間節(jié)點(diǎn),然后看一下具體做了什么樣的操作,才造成這樣現(xiàn)象的產(chǎn)生,接著在代碼中進(jìn)行處理就可以了。

任務(wù)管理器在做判斷的時(shí)候會(huì)顯得更加簡(jiǎn)單一些,因?yàn)樗褪且粋€(gè)數(shù)值的變化,正常當(dāng)界面渲染完成之后,如果沒有其他額外的操作,那么無論是DOM節(jié)點(diǎn)內(nèi)存,還是我們JavaScript內(nèi)存,都是一個(gè)不變化的數(shù)值,或者變化很小。如果這里存在頻繁的GC操作時(shí),這個(gè)數(shù)值的變化就是瞬間增大,瞬間減小,這樣的節(jié)奏,所以看到這樣的過程也意味著代碼存在頻繁的垃圾回收操作。

頻繁的垃圾回收操作表象上帶來的影響是讓用戶覺得應(yīng)用在使用的時(shí)候非常卡頓,從內(nèi)部看就是當(dāng)前代碼中存在對(duì)內(nèi)存操作不當(dāng)?shù)男袨樽孏C不斷的工作,來回收釋放相應(yīng)的空間。

總結(jié)

到此這篇關(guān)于JavaScript垃圾回收機(jī)制的文章就介紹到這了,更多相關(guān)JavaScript垃圾回收機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論