摘要:根據(jù)的定義,垃圾回收是一種自動的內(nèi)存管理機制。但在沒有結(jié)束前,回調(diào)函數(shù)里的變量以及回調(diào)函數(shù)本身都無法被回收。在內(nèi)存泄漏部分,我們討論了無意的全局變量會帶來無法回收的內(nèi)存垃圾。
根據(jù) Wiki 的定義,垃圾回收是一種自動的內(nèi)存管理機制。當(dāng)計算機上的動態(tài)內(nèi)存不再需要時,就應(yīng)該予以釋放,以讓出內(nèi)存。直白點講,就是程序是運行在內(nèi)存里的,當(dāng)聲明一個變量、定義一個函數(shù)時都會占用內(nèi)存。內(nèi)存的容量是有限的,如果變量、函數(shù)等只有產(chǎn)生沒有消亡的過程,那遲早內(nèi)存有被完全占用的時候。這個時候,不僅自己的程序無法正常運行,連其他程序也會受到影響。好比生物只有出生沒有死亡,地球總有被撐爆的一天。所以,在計算機中,我們需要垃圾回收。需要注意的是,定義中的“自動”的意思是語言可以幫助我們回收內(nèi)存垃圾,但并不代表我們不用關(guān)心內(nèi)存管理,如果操作失當(dāng),JavaScript 中依舊會出現(xiàn)內(nèi)存溢出的情況。
垃圾回收基于兩個原理:
考慮某個變量或?qū)ο笤谖磥淼某绦蜻\行中將不會被訪問
向這些對象要求歸還內(nèi)存
而這兩個原理中,最主要的也是最艱難的部分就是找到“所分配的內(nèi)存確實已經(jīng)不再需要了”。
垃圾回收方法下面我們看看在 JavaScript 中是如何找到不再使用的內(nèi)存的。主要有兩種方式:引用計數(shù)和標(biāo)記清除。
引用計數(shù)(reference counting)在內(nèi)存管理環(huán)境中,對象 A 如果有訪問對象 B 的權(quán)限,叫做對象 A 引用對象 B。引用計數(shù)的策略是將“對象是否不再需要”簡化成“對象有沒有其他對象引用到它”,如果沒有對象引用這個對象,那么這個對象將會被回收。上例子:
let obj1 = { a: 1 }; // 一個對象(稱之為 A)被創(chuàng)建,賦值給 obj1,A 的引用個數(shù)為 1 let obj2 = obj1; // A 的引用個數(shù)變?yōu)?2 obj1 = 0; // A 的引用個數(shù)變?yōu)?1 obj2 = 0; // A 的引用個數(shù)變?yōu)?0,此時對象 A 就可以被垃圾回收了
但是引用計數(shù)有個最大的問題: 循環(huán)引用。
function func() { let obj1 = {}; let obj2 = {}; obj1.a = obj2; // obj1 引用 obj2 obj2.a = obj1; // obj2 引用 obj1 }
當(dāng)函數(shù) func 執(zhí)行結(jié)束后,返回值為 undefined,所以整個函數(shù)以及內(nèi)部的變量都應(yīng)該被回收,但根據(jù)引用計數(shù)方法,obj1 和 obj2 的引用次數(shù)都不為 0,所以他們不會被回收。
要解決循環(huán)引用的問題,最好是在不使用它們的時候手工將它們設(shè)為空。上面的例子可以這么做:
obj1 = null; obj2 = null;標(biāo)記-清除(mark and sweep)
這是 JavaScript 中最常見的垃圾回收方式。為什么說這是種最常見的方法,因為從 2012 年起,所有現(xiàn)代瀏覽器都使用了標(biāo)記-清除的垃圾回收方法,除了低版本 IE...它們采用的是引用計數(shù)方法。
那什么叫標(biāo)記清除呢?JavaScript 中有個全局對象,瀏覽器中是 window。定期的,垃圾回收期將從這個全局對象開始,找所有從這個全局對象開始引用的對象,再找這些對象引用的對象...對這些活著的對象進(jìn)行標(biāo)記,這是標(biāo)記階段。清除階段就是清除那些沒有被標(biāo)記的對象。
標(biāo)記-清除法的一個問題就是不那么有效率,因為在標(biāo)記-清除階段,整個程序?qū)却匀绻绦虺霈F(xiàn)卡頓的情況,那有可能是收集垃圾的過程。
2012 年起,所有現(xiàn)代瀏覽器都使用了這個方法,所有的改進(jìn)也都是基于這個方法,比如標(biāo)記-整理方法。
標(biāo)記清除有一個問題,就是在清除之后,內(nèi)存空間是不連續(xù)的,即出現(xiàn)了內(nèi)存碎片。如果后面需要一個比較大的連續(xù)的內(nèi)存空間時,那將不能滿足要求。而標(biāo)記-整理方法可以有效地解決這個問題。標(biāo)記階段沒有什么不同,只是標(biāo)記結(jié)束后,標(biāo)記-整理方法會將活著的對象向內(nèi)存的一邊移動,最后清理掉邊界的內(nèi)存。不過可以想象,這種做法的效率沒有標(biāo)記-清除高。計算機中的很多做法都是互相妥協(xié)的結(jié)果,哪有什么十全十美的事兒呢。
內(nèi)存泄漏在談什么是良好實踐(這里指有益于內(nèi)存管理)之前,我想先談?wù)剝?nèi)存泄漏,也就是差的實踐。內(nèi)存泄漏是指計算機可用的內(nèi)存越來越少,主要是因為程序不能釋放那些不再使用的內(nèi)存。
循環(huán)引用這個沒什么好說的,上面已經(jīng)介紹了。
需要強調(diào)的一點就是,一旦數(shù)據(jù)不再使用,最好通過將其值設(shè)為 null 來釋放其引用,這個方法被稱為“解除引用”。
無意的全局變量function foo(arg) { const bar = ""; } foo();
當(dāng) foo 函數(shù)執(zhí)行后,變量 bar 就會被標(biāo)記為可回收。因為當(dāng)函數(shù)執(zhí)行時,函數(shù)創(chuàng)造了一個作用域來讓函數(shù)里的變量在里面聲明。進(jìn)入這個作用域后,瀏覽器就會為變量 bar 創(chuàng)建一個內(nèi)存空間。當(dāng)這個函數(shù)結(jié)束后,其所創(chuàng)建的作用域里的變量也會被標(biāo)記為垃圾,在下一個垃圾回收周期到來時,這些變量將會被回收。
但事情并不會那么順利。
function foo(arg) { bar = ""; } foo();
上面的代碼就無意中聲明了一個全局變量,會得到 window 的引用,bar 實際上是 window.bar,它的作用域在 window 上,所以 foo 函數(shù)執(zhí)行結(jié)束后,bar 也不會被內(nèi)存收回。
另外一種無意的全局變量的情況是:
function foo() { this.bar = ""; }
在 foo 函數(shù)中,this 指的是 window(詳細(xì)內(nèi)容可參見我的另一篇博客:JavaScript this 講解),犯的錯誤跟上面類似。
被遺忘的計時器和回調(diào)函數(shù)let someResource = getData(); setInterval(() => { const node = document.getElementById("Node"); if(node) { node.innerHTML = JSON.stringify(someResource)); } }, 1000);
上面的例子中,我們每隔一秒就將得到的數(shù)據(jù)放入到文檔節(jié)點中去。但在 setInterval 沒有結(jié)束前,回調(diào)函數(shù)里的變量以及回調(diào)函數(shù)本身都無法被回收。那什么才叫結(jié)束呢?就是調(diào)用了 clearInterval。如果回調(diào)函數(shù)內(nèi)沒有做什么事情,并且也沒有被 clear 掉的話,就會造成內(nèi)存泄漏。不僅如此,如果回調(diào)函數(shù)沒有被回收,那么回調(diào)函數(shù)內(nèi)依賴的變量也沒法被回收。上面的例子中,someResource 就沒法被回收。同樣的,setTiemout 也會有同樣的問題。所以,當(dāng)不需要 interval 或者 timeout 時,最好調(diào)用 clearInterval 或者 clearTimeout。
DOM在 IE8 以下的版本里,DOM 對象經(jīng)常會跟 JavaScript 之間產(chǎn)生循環(huán)引用。看一個例子:
function setHandler() { const ele = document.getElementById("id"); ele.onclick = function() {}; }
在這個例子中,DOM 對象通過 onclick 引用了一個函數(shù),然而這個函數(shù)通過外部的詞法環(huán)境引用了這個 DOM 對象,形成了循環(huán)引用。不過現(xiàn)在不必?fù)?dān)心,因為所有現(xiàn)代瀏覽器都采用了標(biāo)記-整理方法,避免了循環(huán)引用的問題。
除了這種情況,我們現(xiàn)在還會在其他時候在使用 DOM 時出現(xiàn)內(nèi)存泄漏的問題。當(dāng)我們需要多次訪問同一個 DOM 元素時,一個好的做法是將 DOM 元素用一個變量存儲在內(nèi)存中,因為訪問 DOM 的效率一般比較低,應(yīng)該避免頻繁地反問 DOM 元素。所以我們會這樣寫:
const button = document.getElementById("button");
當(dāng)刪除這個按鈕時:
document.body.removeChild(document.getElementById("button"));
雖然這樣看起來刪除了這個 DOM 元素,但這個 DOM 元素仍然被 button 這個變量引用,所以在內(nèi)存上,這個 DOM 元素是沒法被回收的。所以在使用結(jié)束后,還需要將 button 設(shè)成 null。
另外一個值得注意的是,代碼中保存了一個列表 ul 的某一項 li 的引用,將來決定刪除整個列表時,我們自覺上會認(rèn)為內(nèi)存僅僅會保留那個特定的 li,而將其他列表項都刪除。但事實并非如此,因為 li 是 ul 的子元素,子元素與父元素是引用關(guān)系,所以如果代碼保存 li 的引用,那么整個 ul 將會繼續(xù)呆在內(nèi)存里。
良好實踐1、優(yōu)化內(nèi)存的一個最好的衡量方式就是只保留程序運行時需要的數(shù)據(jù),對于已經(jīng)使用的或者不需要的數(shù)據(jù),應(yīng)該將其值設(shè)為 null,這上面說過,叫“解除引用”。需要注意的是,解除一個值的引用不代表垃圾回收器會立即將這段內(nèi)存回收,這樣做的目的是讓垃圾回收器在下一個回收周期到來時知道這段內(nèi)存需要回收。
在內(nèi)存泄漏部分,我們討論了無意的全局變量會帶來無法回收的內(nèi)存垃圾。但有些時候,我們會有意識地聲明一些全局變量,這個時候需要注意,如果聲明的變量占用大量的內(nèi)存,那么在使用完后將變量聲明為 null。
2、減少內(nèi)存垃圾的另一個方法就是避免創(chuàng)建對象。new Object() 是一個比較明顯的創(chuàng)建對象的方式,另外 const arr = [];、const obj = {};也會創(chuàng)建新的對象。另外下面這種寫法在每次調(diào)用函數(shù)時都會創(chuàng)建一個新的對象:
function func() { return function() {}; }
另外,當(dāng)清空一個數(shù)組時,我們通常的做法是 array = [],但這種做法的背后是新建了一個新的數(shù)組然后將原來的數(shù)組當(dāng)作內(nèi)存垃圾。建議的做法是 array.length = 0,這樣做不僅可以重用原來的變量,而且還避免創(chuàng)建了新的數(shù)組。
?
因為時間關(guān)系,關(guān)于垃圾回收的內(nèi)容將在接下來1-2周內(nèi)更新完畢,內(nèi)容涉及更加詳細(xì)的內(nèi)存管理、V8 引擎中的垃圾回收等。另外對本文其他內(nèi)容還有建議的也歡迎留言,我也會一并更新。
參考:
內(nèi)存管理
A tour of V8: Garbage Collection
Memory leaks
4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
High-Performance, Garbage-Collector-Friendly Code
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/81055.html
摘要:摘要是如何回收內(nèi)存的深入淺出系列深入淺出第課箭頭函數(shù)中的究竟是什么鬼深入淺出第課函數(shù)是一等公民是什么意思呢深入淺出第課什么是垃圾回收算法最近垃圾回收這個話題非常火,大家不能隨隨便便的扔垃圾了,還得先分類,這樣方便對垃圾進(jìn)行回收再利用。 摘要: JS是如何回收內(nèi)存的? 《JavaScript深入淺出》系列: JavaScript深入淺出第1課:箭頭函數(shù)中的this究竟是什么鬼? Jav...
摘要:它將堆內(nèi)存一分為二每一部分空間稱為。以的垃圾回收堆內(nèi)存為例做一次小的垃圾回收需要毫秒以上做一次非增量式的垃圾回收甚至要秒以上。這是垃圾回收中引起線程暫停執(zhí)行的時間在這樣的時間花銷下應(yīng)用的性能和響應(yīng)能力都會直線下降。 我們通常理解的 javascript 垃圾回收機制都停留在表面,會釋放不被引用變量內(nèi)存,最近在讀《深入淺出node.js》的書,詳細(xì)了解了下 v8 垃圾回收的算法,記錄了一...
摘要:內(nèi)存回收此時,局部變量就沒有存在的必要了,因此可以釋放它們的內(nèi)存以供將來使用。局部變量會在它們離開執(zhí)行環(huán)境時自動被解除引用,如下面這個例子所示手工解除的引用由于局部變量在函數(shù)執(zhí)行完畢后就離開了其執(zhí)行環(huán)境,因此無需我們顯式地去為它解除引用。 JavaScript 具有自動垃圾收集機制(GC:Garbage Collecation),也就是說,執(zhí)行環(huán)境會負(fù)責(zé)管理代碼執(zhí)行過程中使用的內(nèi)存。而...
摘要:所謂的內(nèi)存泄漏簡單來說是不再用到的內(nèi)存,沒有及時釋放。如果一個值不再需要了,引用數(shù)卻不為,垃圾回收機制無法釋放這塊內(nèi)存,從而導(dǎo)致內(nèi)存泄漏。 前言 程序的運行需要內(nèi)存。只要程序提出要求,操作系統(tǒng)或者運行時就必須供給內(nèi)存。所謂的內(nèi)存泄漏簡單來說是不再用到的內(nèi)存,沒有及時釋放。為了更好避免內(nèi)存泄漏,我們先介紹Javascript垃圾回收機制。 在C與C++等語言中,開發(fā)人員可以直接控制內(nèi)存的...
摘要:所謂的內(nèi)存泄漏簡單來說是不再用到的內(nèi)存,沒有及時釋放。如果一個值不再需要了,引用數(shù)卻不為,垃圾回收機制無法釋放這塊內(nèi)存,從而導(dǎo)致內(nèi)存泄漏。 前言 程序的運行需要內(nèi)存。只要程序提出要求,操作系統(tǒng)或者運行時就必須供給內(nèi)存。所謂的內(nèi)存泄漏簡單來說是不再用到的內(nèi)存,沒有及時釋放。為了更好避免內(nèi)存泄漏,我們先介紹Javascript垃圾回收機制。 在C與C++等語言中,開發(fā)人員可以直接控制內(nèi)存的...
摘要:另一種垃圾收集算法是引用計數(shù),這種算法的思想是跟蹤記錄所有值被引用的次數(shù)。當(dāng)代碼中存在循環(huán)引用現(xiàn)象時,引用計數(shù)算法就會導(dǎo)致問題。 垃圾回收 javascript不同于c、c++的一個特點是:具有自動的垃圾回收機制,這就意味著,開發(fā)人員可以專注于業(yè)務(wù),而不必把過多精力放在內(nèi)存的管理上,提高開發(fā)效率。 所謂的垃圾回收就是找出那些不再繼續(xù)使用的變量,然后釋放其占用的內(nèi)存。為此,垃圾收集器...
閱讀 3820·2021-10-12 10:12
閱讀 1453·2021-10-11 10:58
閱讀 2290·2021-10-09 10:01
閱讀 2597·2021-09-24 09:48
閱讀 2699·2021-09-09 11:38
閱讀 3526·2019-08-30 15:44
閱讀 1724·2019-08-30 14:22
閱讀 518·2019-08-29 12:42