自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

中高級(jí)前端必須了解的JS中的內(nèi)存管理

開(kāi)發(fā) 前端
像C語(yǔ)言這樣的底層語(yǔ)言一般都有底層的內(nèi)存管理接口,比如 malloc()和free()用于分配內(nèi)存和釋放內(nèi)存。

前言

像C語(yǔ)言這樣的底層語(yǔ)言一般都有底層的內(nèi)存管理接口,比如 malloc()和free()用于分配內(nèi)存和釋放內(nèi)存。

[[269478]]

而對(duì)于JavaScript來(lái)說(shuō),會(huì)在創(chuàng)建變量(對(duì)象,字符串等)時(shí)分配內(nèi)存,并且在不再使用它們時(shí)“自動(dòng)”釋放內(nèi)存,這個(gè)自動(dòng)釋放內(nèi)存的過(guò)程稱為垃圾回收。

因?yàn)樽詣?dòng)垃圾回收機(jī)制的存在,讓大多Javascript開(kāi)發(fā)者感覺(jué)他們可以不關(guān)心內(nèi)存管理,所以會(huì)在一些情況下導(dǎo)致內(nèi)存泄漏。

內(nèi)存生命周期

中高級(jí)前端必須了解的JS中的內(nèi)存管理

JS 環(huán)境中分配的內(nèi)存有如下聲明周期:

  1. 內(nèi)存分配:當(dāng)我們申明變量、函數(shù)、對(duì)象的時(shí)候,系統(tǒng)會(huì)自動(dòng)為他們分配內(nèi)存
  2. 內(nèi)存使用:即讀寫(xiě)內(nèi)存,也就是使用變量、函數(shù)等
  3. 內(nèi)存回收:使用完畢,由垃圾回收機(jī)制自動(dòng)回收不再使用的內(nèi)存

JS 的內(nèi)存分配

為了不讓程序員費(fèi)心分配內(nèi)存,JavaScript 在定義變量時(shí)就完成了內(nèi)存分配。

  1. var n = 123; // 給數(shù)值變量分配內(nèi)存 
  2. var s = "azerty"; // 給字符串分配內(nèi)存 
  3. var o = { 
  4.  a: 1, 
  5.  b: null 
  6. }; // 給對(duì)象及其包含的值分配內(nèi)存 
  7. // 給數(shù)組及其包含的值分配內(nèi)存(就像對(duì)象一樣) 
  8. var a = [1, null"abra"];  
  9. function f(a){ 
  10.  return a + 2; 
  11. } // 給函數(shù)(可調(diào)用的對(duì)象)分配內(nèi)存 
  12. // 函數(shù)表達(dá)式也能分配一個(gè)對(duì)象 
  13. someElement.addEventListener('click'function(){ 
  14.  someElement.style.backgroundColor = 'blue'
  15. }, false); 

有些函數(shù)調(diào)用結(jié)果是分配對(duì)象內(nèi)存:

  1. var d = new Date(); // 分配一個(gè) Date 對(duì)象 
  2. var e = document.createElement('div'); // 分配一個(gè) DOM 元素 

有些方法分配新變量或者新對(duì)象:

  1. var s = "azerty"
  2. var s2 = s.substr(0, 3); // s2 是一個(gè)新的字符串 
  3. // 因?yàn)樽址遣蛔兞浚?nbsp;
  4. // JavaScript 可能決定不分配內(nèi)存, 
  5. // 只是存儲(chǔ)了 [0-3] 的范圍。 
  6. var a = ["ouais ouais""nan nan"]; 
  7. var a2 = ["generation""nan nan"]; 
  8. var a3 = a.concat(a2);  
  9. // 新數(shù)組有四個(gè)元素,是 a 連接 a2 的結(jié)果 

JS 的內(nèi)存使用

使用值的過(guò)程實(shí)際上是對(duì)分配內(nèi)存進(jìn)行讀取與寫(xiě)入的操作。

讀取與寫(xiě)入可能是寫(xiě)入一個(gè)變量或者一個(gè)對(duì)象的屬性值,甚至傳遞函數(shù)的參數(shù)。

  1. var a = 10; // 分配內(nèi)存 
  2. console.log(a); // 對(duì)內(nèi)存的使用 

JS 的內(nèi)存回收

JS 有自動(dòng)垃圾回收機(jī)制,那么這個(gè)自動(dòng)垃圾回收機(jī)制的原理是什么呢?

其實(shí)很簡(jiǎn)單,就是找出那些不再繼續(xù)使用的值,然后釋放其占用的內(nèi)存。

大多數(shù)內(nèi)存管理的問(wèn)題都在這個(gè)階段。

在這里最艱難的任務(wù)是找到不再需要使用的變量。

不再需要使用的變量也就是生命周期結(jié)束的變量,是局部變量,局部變量只在函數(shù)的執(zhí)行過(guò)程中存在,

當(dāng)函數(shù)運(yùn)行結(jié)束,沒(méi)有其他引用(閉包),那么該變量會(huì)被標(biāo)記回收。

全局變量的生命周期直至瀏覽器卸載頁(yè)面才會(huì)結(jié)束,也就是說(shuō)全局變量不會(huì)被當(dāng)成垃圾回收。

因?yàn)樽詣?dòng)垃圾回收機(jī)制的存在,開(kāi)發(fā)人員可以不關(guān)心也不注意內(nèi)存釋放的有關(guān)問(wèn)題,但對(duì)無(wú)用內(nèi)存的釋放這件事是客觀存在的。

不幸的是,即使不考慮垃圾回收對(duì)性能的影響,目前***的垃圾回收算法,也無(wú)法智能回收所有的極端情況。

接下來(lái)我們來(lái)探究一下 JS 垃圾回收的機(jī)制。

垃圾回收

引用

垃圾回收算法主要依賴于引用的概念。

在內(nèi)存管理的環(huán)境中,一個(gè)對(duì)象如果有訪問(wèn)另一個(gè)對(duì)象的權(quán)限(隱式或者顯式),叫做一個(gè)對(duì)象引用另一個(gè)對(duì)象。

例如,一個(gè)Javascript對(duì)象具有對(duì)它原型的引用(隱式引用)和對(duì)它屬性的引用(顯式引用)。

在這里,“對(duì)象”的概念不僅特指 JavaScript 對(duì)象,還包括函數(shù)作用域(或者全局詞法作用域)。

引用計(jì)數(shù)垃圾收集

這是最初級(jí)的垃圾回收算法。

引用計(jì)數(shù)算法定義“內(nèi)存不再使用”的標(biāo)準(zhǔn)很簡(jiǎn)單,就是看一個(gè)對(duì)象是否有指向它的引用。

如果沒(méi)有其他對(duì)象指向它了,說(shuō)明該對(duì)象已經(jīng)不再需要了。

  1. var o = {  
  2.  a: { 
  3.  b:2 
  4.  } 
  5. };  
  6. // 兩個(gè)對(duì)象被創(chuàng)建,一個(gè)作為另一個(gè)的屬性被引用,另一個(gè)被分配給變量o 
  7. // 很顯然,沒(méi)有一個(gè)可以被垃圾收集 
  8. var o2 = o; // o2變量是第二個(gè)對(duì)“這個(gè)對(duì)象”的引用 
  9. o = 1; // 現(xiàn)在,“這個(gè)對(duì)象”的原始引用o被o2替換了 
  10. var oa = o2.a; // 引用“這個(gè)對(duì)象”的a屬性 
  11. // 現(xiàn)在,“這個(gè)對(duì)象”有兩個(gè)引用了,一個(gè)是o2,一個(gè)是oa 
  12. o2 = "yo"; // 最初的對(duì)象現(xiàn)在已經(jīng)是零引用了 
  13.  // 他可以被垃圾回收了 
  14.  // 然而它的屬性a的對(duì)象還在被oa引用,所以還不能回收 
  15. oa = null; // a屬性的那個(gè)對(duì)象現(xiàn)在也是零引用了 
  16.  // 它可以被垃圾回收了 

由上面可以看出,引用計(jì)數(shù)算法是個(gè)簡(jiǎn)單有效的算法。但它卻存在一個(gè)致命的問(wèn)題:循環(huán)引用。

如果兩個(gè)對(duì)象相互引用,盡管他們已不再使用,垃圾回收不會(huì)進(jìn)行回收,導(dǎo)致內(nèi)存泄露。

來(lái)看一個(gè)循環(huán)引用的例子:

  1. function f(){ 
  2.  var o = {}; 
  3.  var o2 = {}; 
  4.  o.a = o2; // o 引用 o2 
  5.  o2.a = o; // o2 引用 o 這里 
  6.  return "azerty"
  7. f(); 

上面我們申明了一個(gè)函數(shù) f ,其中包含兩個(gè)相互引用的對(duì)象。

在調(diào)用函數(shù)結(jié)束后,對(duì)象 o1 和 o2 實(shí)際上已離開(kāi)函數(shù)范圍,因此不再需要了。

但根據(jù)引用計(jì)數(shù)的原則,他們之間的相互引用依然存在,因此這部分內(nèi)存不會(huì)被回收,內(nèi)存泄露不可避免了。

再來(lái)看一個(gè)實(shí)際的例子:

  1. var div = document.createElement("div"); 
  2. div.onclick = function() { 
  3.  console.log("click"); 
  4. }; 

上面這種JS寫(xiě)法再普通不過(guò)了,創(chuàng)建一個(gè)DOM元素并綁定一個(gè)點(diǎn)擊事件。

此時(shí)變量 div 有事件處理函數(shù)的引用,同時(shí)事件處理函數(shù)也有div的引用!(div變量可在函數(shù)內(nèi)被訪問(wèn))。

一個(gè)循序引用出現(xiàn)了,按上面所講的算法,該部分內(nèi)存無(wú)可避免的泄露了。

為了解決循環(huán)引用造成的問(wèn)題,現(xiàn)代瀏覽器通過(guò)使用標(biāo)記清除算法來(lái)實(shí)現(xiàn)垃圾回收。

標(biāo)記清除算法

標(biāo)記清除算法將“不再使用的對(duì)象”定義為“無(wú)法達(dá)到的對(duì)象”。

簡(jiǎn)單來(lái)說(shuō),就是從根部(在JS中就是全局對(duì)象)出發(fā)定時(shí)掃描內(nèi)存中的對(duì)象。

凡是能從根部到達(dá)的對(duì)象,都是還需要使用的。

那些無(wú)法由根部出發(fā)觸及到的對(duì)象被標(biāo)記為不再使用,稍后進(jìn)行回收。

從這個(gè)概念可以看出,無(wú)法觸及的對(duì)象包含了沒(méi)有引用的對(duì)象這個(gè)概念(沒(méi)有任何引用的對(duì)象也是無(wú)法觸及的對(duì)象)。

但反之未必成立。

工作流程:

  1. 垃圾收集器會(huì)在運(yùn)行的時(shí)候會(huì)給存儲(chǔ)在內(nèi)存中的所有變量都加上標(biāo)記。
  2. 從根部出發(fā)將能觸及到的對(duì)象的標(biāo)記清除。
  3. 那些還存在標(biāo)記的變量被視為準(zhǔn)備刪除的變量。
  4. ***垃圾收集器會(huì)執(zhí)行***一步內(nèi)存清除的工作,銷毀那些帶標(biāo)記的值并回收它們所占用的內(nèi)存空間。
中高級(jí)前端必須了解的JS中的內(nèi)存管理

循環(huán)引用不再是問(wèn)題了

再看之前循環(huán)引用的例子:

  1. function f(){ 
  2.  var o = {}; 
  3.  var o2 = {}; 
  4.  o.a = o2; // o 引用 o2 
  5.  o2.a = o; // o2 引用 o 
  6.  return "azerty"
  7. f(); 

函數(shù)調(diào)用返回之后,兩個(gè)循環(huán)引用的對(duì)象在垃圾收集時(shí)從全局對(duì)象出發(fā)無(wú)法再獲取他們的引用。

因此,他們將會(huì)被垃圾回收器回收。

內(nèi)存泄漏

什么是內(nèi)存泄漏

程序的運(yùn)行需要內(nèi)存。只要程序提出要求,操作系統(tǒng)或者運(yùn)行時(shí)(runtime)就必須供給內(nèi)存。

對(duì)于持續(xù)運(yùn)行的服務(wù)進(jìn)程(daemon),必須及時(shí)釋放不再用到的內(nèi)存。

否則,內(nèi)存占用越來(lái)越高,輕則影響系統(tǒng)性能,重則導(dǎo)致進(jìn)程崩潰。

本質(zhì)上講,內(nèi)存泄漏就是由于疏忽或錯(cuò)誤造成程序未能釋放那些已經(jīng)不再使用的內(nèi)存,造成內(nèi)存的浪費(fèi)。

內(nèi)存泄漏的識(shí)別方法

經(jīng)驗(yàn)法則是,如果連續(xù)五次垃圾回收之后,內(nèi)存占用一次比一次大,就有內(nèi)存泄漏。

這就要求實(shí)時(shí)查看內(nèi)存的占用情況。

在 Chrome 瀏覽器中,我們可以這樣查看內(nèi)存占用情況

  1. 打開(kāi)開(kāi)發(fā)者工具,選擇 Performance 面板
  2. 在頂部勾選 Memory
  3. 點(diǎn)擊左上角的 record 按鈕
  4. 在頁(yè)面上進(jìn)行各種操作,模擬用戶的使用情況
  5. 一段時(shí)間后,點(diǎn)擊對(duì)話框的 stop 按鈕,面板上就會(huì)顯示這段時(shí)間的內(nèi)存占用情況

來(lái)看一張效果圖:

中高級(jí)前端必須了解的JS中的內(nèi)存管理

我們有兩種方式來(lái)判定當(dāng)前是否有內(nèi)存泄漏:

  1. 多次快照后,比較每次快照中內(nèi)存的占用情況,如果呈上升趨勢(shì),那么可以認(rèn)為存在內(nèi)存泄漏
  2. 某次快照后,看當(dāng)前內(nèi)存占用的趨勢(shì)圖,如果走勢(shì)不平穩(wěn),呈上升趨勢(shì),那么可以認(rèn)為存在內(nèi)存泄漏

在服務(wù)器環(huán)境中使用 Node 提供的 process.memoryUsage 方法查看內(nèi)存情況

  1. console.log(process.memoryUsage()); 
  2. // {  
  3. // rss: 27709440, 
  4. // heapTotal: 5685248, 
  5. // heapUsed: 3449392, 
  6. // external: 8772  
  7. // } 

process.memoryUsage返回一個(gè)對(duì)象,包含了 Node 進(jìn)程的內(nèi)存占用信息。

該對(duì)象包含四個(gè)字段,單位是字節(jié),含義如下:

  • rss(resident set size):所有內(nèi)存占用,包括指令區(qū)和堆棧。
  • heapTotal:"堆"占用的內(nèi)存,包括用到的和沒(méi)用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎內(nèi)部的 C++ 對(duì)象占用的內(nèi)存。

判斷內(nèi)存泄漏,以heapUsed字段為準(zhǔn)。

常見(jiàn)的內(nèi)存泄露案例

意外的全局變量

  1. function foo() { 
  2.  bar1 = 'some text'; // 沒(méi)有聲明變量 實(shí)際上是全局變量 => window.bar1 
  3.  this.bar2 = 'some text' // 全局變量 => window.bar2 
  4. foo(); 

在這個(gè)例子中,意外的創(chuàng)建了兩個(gè)全局變量 bar1 和 bar2

被遺忘的定時(shí)器和回調(diào)函數(shù)

在很多庫(kù)中, 如果使用了觀察者模式, 都會(huì)提供回調(diào)方法, 來(lái)調(diào)用一些回調(diào)函數(shù)。

要記得回收這些回調(diào)函數(shù)。舉一個(gè) setInterval的例子:

  1. var serverData = loadData(); 
  2. setInterval(function() { 
  3.  var renderer = document.getElementById('renderer'); 
  4.  if(renderer) { 
  5.  renderer.innerHTML = JSON.stringify(serverData); 
  6.  } 
  7. }, 5000); // 每 5 秒調(diào)用一次 

如果后續(xù) renderer 元素被移除,整個(gè)定時(shí)器實(shí)際上沒(méi)有任何作用。

但如果你沒(méi)有回收定時(shí)器,整個(gè)定時(shí)器依然有效, 不但定時(shí)器無(wú)法被內(nèi)存回收,

定時(shí)器函數(shù)中的依賴也無(wú)法回收。在這個(gè)案例中的 serverData 也無(wú)法被回收。

閉包

在 JS 開(kāi)發(fā)中,我們會(huì)經(jīng)常用到閉包,一個(gè)內(nèi)部函數(shù),有權(quán)訪問(wèn)包含其的外部函數(shù)中的變量。

下面這種情況下,閉包也會(huì)造成內(nèi)存泄露:

  1. var theThing = null
  2. var replaceThing = function () { 
  3.  var originalThing = theThing; 
  4.  var unused = function () { 
  5.  if (originalThing) // 對(duì)于 'originalThing'的引用 
  6.  console.log("hi"); 
  7.  }; 
  8.  theThing = { 
  9.  longStr: new Array(1000000).join('*'), 
  10.  someMethod: function () { 
  11.  console.log("message"); 
  12.  } 
  13.  }; 
  14. }; 
  15. setInterval(replaceThing, 1000); 

這段代碼,每次調(diào)用 replaceThing 時(shí),theThing 獲得了包含一個(gè)巨大的數(shù)組和一個(gè)對(duì)于新閉包 someMethod 的對(duì)象。

同時(shí) unused 是一個(gè)引用了 originalThing 的閉包。

這個(gè)范例的關(guān)鍵在于,閉包之間是共享作用域的,盡管 unused 可能一直沒(méi)有被調(diào)用,但是 someMethod 可能會(huì)被調(diào)用,就會(huì)導(dǎo)致無(wú)法對(duì)其內(nèi)存進(jìn)行回收。

當(dāng)這段代碼被反復(fù)執(zhí)行時(shí),內(nèi)存會(huì)持續(xù)增長(zhǎng)。

DOM 引用

很多時(shí)候, 我們對(duì) Dom 的操作, 會(huì)把 Dom 的引用保存在一個(gè)數(shù)組或者 Map 中。

  1. var elements = { 
  2.  image: document.getElementById('image'
  3. }; 
  4. function doStuff() { 
  5.  elements.image.src = 'http://example.com/image_name.png'
  6. function removeImage() { 
  7.  document.body.removeChild(document.getElementById('image')); 
  8.  // 這個(gè)時(shí)候我們對(duì)于 #image 仍然有一個(gè)引用, Image 元素, 仍然無(wú)法被內(nèi)存回收. 

上述案例中,即使我們對(duì)于 image 元素進(jìn)行了移除,但是仍然有對(duì) image 元素的引用,依然無(wú)法對(duì)齊進(jìn)行內(nèi)存回收。

另外需要注意的一個(gè)點(diǎn)是,對(duì)于一個(gè) Dom 樹(shù)的葉子節(jié)點(diǎn)的引用。

舉個(gè)例子: 如果我們引用了一個(gè)表格中的td元素,一旦在 Dom 中刪除了整個(gè)表格,我們直觀的覺(jué)得內(nèi)存回收應(yīng)該回收除了被引用的 td 外的其他元素。

但是事實(shí)上,這個(gè) td 元素是整個(gè)表格的一個(gè)子元素,并保留對(duì)于其父元素的引用。

這就會(huì)導(dǎo)致對(duì)于整個(gè)表格,都無(wú)法進(jìn)行內(nèi)存回收。所以我們要小心處理對(duì)于 Dom 元素的引用。

如何避免內(nèi)存泄漏

記住一個(gè)原則:不用的東西,及時(shí)歸還。

  1. 減少不必要的全局變量,使用嚴(yán)格模式避免意外創(chuàng)建全局變量。
  2. 在你使用完數(shù)據(jù)后,及時(shí)解除引用(閉包中的變量,dom引用,定時(shí)器清除)。
  3. 組織好你的邏輯,避免死循環(huán)等造成瀏覽器卡頓,崩潰的問(wèn)題。
責(zé)任編輯:華軒 來(lái)源: 今日頭條
相關(guān)推薦

2019-08-01 10:57:52

開(kāi)發(fā)者技能TypeScript

2021-09-09 07:21:26

TypeScript 高級(jí)類型

2011-07-28 11:12:25

Cocoa 內(nèi)存

2009-12-16 16:02:21

華為無(wú)線路由器配置

2019-02-20 14:10:22

2022-01-05 14:02:31

前端Nginx單頁(yè)加載

2018-05-30 13:42:39

2022-06-12 23:43:19

SQL數(shù)據(jù)函數(shù)

2009-09-22 18:26:48

2022-03-22 23:18:55

SQL技術(shù)內(nèi)部概念

2024-05-17 16:18:27

2017-10-29 06:50:30

前端開(kāi)發(fā)CSSWeb

2017-06-05 13:56:34

前端開(kāi)發(fā)JavaScriptthis

2009-08-02 10:21:39

ASP.NET程序員面ASP.NET

2015-05-07 15:13:22

JS實(shí)現(xiàn)JQueryJQuery

2021-06-07 07:01:43

js關(guān)鍵字運(yùn)行

2023-10-26 11:19:21

指針Go

2010-04-07 10:31:05

鴻海郭臺(tái)銘

2014-02-10 10:13:43

2010-07-27 11:29:43

Flex
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)