日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

作者:京東零售 謝天

在任何語言開發(fā)的過程中,對于內(nèi)存的管理都非常重要,JAVAscript 也不例外。

然而在前端瀏覽器中,用戶一般不會在一個頁面停留很久,即使有一點內(nèi)存泄漏,重新加載頁面內(nèi)存也會跟著釋放。而且瀏覽器也有自己的自動回收內(nèi)存的機制,所以前端并沒有特別關(guān)注內(nèi)存泄漏的問題。

但是如果我們對內(nèi)存泄漏沒有什么概念,有時候還是有可能因為內(nèi)存泄漏,導(dǎo)致頁面卡頓。了解內(nèi)存泄漏,如何避免內(nèi)存泄漏,都是不可缺少的。

什么是內(nèi)存

在硬件級別上,計算機內(nèi)存由大量觸發(fā)器組成。每個觸發(fā)器包含幾個晶體管,能夠存儲一個位。單個觸發(fā)器可以通過唯一標(biāo)識符尋址,因此我們可以讀取和覆蓋它們。因此,從概念上講,我們可以把我們的整個計算機內(nèi)存看作是一個巨大的位數(shù)組,我們可以讀和寫。

 

這是內(nèi)存的底層概念,JavaScript 作為一個高級語言,不需要通過二進(jìn)制進(jìn)行內(nèi)存的讀寫,而是相關(guān)的 JavaScript 引擎做了這部分的工作。

內(nèi)存的生命周期

內(nèi)存也會有生命周期,不管什么程序語言,一般可以按照順序分為三個周期:

 

  • 分配期:分配所需要的內(nèi)存
  • 使用期:使用分配的內(nèi)存進(jìn)行讀寫
  • 釋放期:不需要時將其釋放和歸還

 

內(nèi)存分配 -> 內(nèi)存使用 -> 內(nèi)存釋放

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

在計算機科學(xué)中,內(nèi)存泄漏指由于疏忽或錯誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存。內(nèi)存泄漏并非指內(nèi)存在物理上的消失,而是應(yīng)用程序分配某段內(nèi)存后,由于設(shè)計錯誤,導(dǎo)致在釋放該段內(nèi)存之前就失去了對該段內(nèi)存的控制,從而造成了內(nèi)存的浪費。

 

如果內(nèi)存不需要時,沒有經(jīng)過生命周期的的釋放期,那么就存在內(nèi)存泄漏

內(nèi)存泄漏的簡單理解:無用的內(nèi)存還在占用,得不到釋放和歸還。比較嚴(yán)重時,無用的內(nèi)存會持續(xù)遞增,從而導(dǎo)致整個系統(tǒng)的卡頓,甚至崩潰。

JavaScript 內(nèi)存管理機制

像 C 語言這樣的底層語言一般都有底層的內(nèi)存管理接口,但是 JavaScript 是在創(chuàng)建變量時自動進(jìn)行了內(nèi)存分配,并且在不使用時自動釋放,釋放的過程稱為“垃圾回收”。然而就是因為自動回收的機制,讓我們錯誤的感覺開發(fā)者不必關(guān)心內(nèi)存的管理。

JavaScript 內(nèi)存管理機制和內(nèi)存的生命周期是一致的,首先需要分配內(nèi)存,然后使用內(nèi)存,最后釋放內(nèi)存。絕大多數(shù)情況下不需要手動釋放內(nèi)存,只需要關(guān)注對內(nèi)存的使用(變量、函數(shù)、對象等)。

內(nèi)存分配

JavaScript 定義變量就會自動分配內(nèi)存,我們只需要了解 JavaScript 的內(nèi)存是自動分配的就可以了。

let num = 1; const str = "名字"; const obj = { a: 1, b: 2 } const arr = [1, 2, 3]; function func (arg) { ... } 內(nèi)存使用

使用值的過程實際上是對分配的內(nèi)存進(jìn)行讀寫的操作,讀取和寫入的操作可能是寫入一個變量或者一個對象的屬性值,甚至傳遞函數(shù)的參數(shù)。

// 繼續(xù)上部分 // 寫入內(nèi)存 num = 2; // 讀取內(nèi)存,寫入內(nèi)存 func(num); 內(nèi)存回收

垃圾回收被稱為 GC(Garbage Collection)

內(nèi)存泄漏一般都是發(fā)生在這一步,JavaScript 的內(nèi)存回收機制雖然可以回收絕大部分的垃圾內(nèi)存,但是還是存在回收不了的情況,如果存在這些情況,需要我們自己手動清理內(nèi)存。

以前一些老版本的瀏覽器的 JavaScript 回收機制沒有那么完善,經(jīng)常出現(xiàn)一些 bug 的內(nèi)存泄漏,不過現(xiàn)在的瀏覽器一般都沒有這個問題了。

這里了解下現(xiàn)在 JavaScript 的垃圾內(nèi)存的兩種回收方式,熟悉一下這兩種算法可以幫助我們理解一些內(nèi)存泄漏的場景。

引用計數(shù)

這是最初級的垃圾收集算法。此算法把“對象是否不再需要”簡化定義為“對象有沒有其他對象引用到它”。如果沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。

// “對象”分配給 obj1 var obj1 = { a: 1, b: 2 } // obj2 引用“對象” var obj2 = obj1; // “對象”的原始引用 obj1 被 obj2 替換 obj1 = 1;

當(dāng)前執(zhí)行環(huán)境中,“對象”內(nèi)存還沒有被回收,需要手動釋放“對象”的內(nèi)存(在沒有離開當(dāng)前執(zhí)行環(huán)境的前提下)

obj2 = null; // 或者 obj2 = 1; // 只要替換“對象”就可以了

這樣引用的“對象”內(nèi)存就被回收了。

ES6 中把引用分為強引用和弱引用,這個目前只有在 Set 和 Map 中才存在。

強引用才會有引用計數(shù)疊加,只有引用計數(shù)為 0 的對象的內(nèi)存才會被回收,所以一般需要手動回收內(nèi)存(手動回收的前提在于標(biāo)記清除法還沒執(zhí)行,還處于當(dāng)前的執(zhí)行環(huán)境)。

而弱引用沒有觸發(fā)引用計數(shù)疊加,只要引用計數(shù)為 0,弱引用就會自動消失,無需手動回收內(nèi)存。

標(biāo)記清除

當(dāng)變量進(jìn)入執(zhí)行時標(biāo)記為“進(jìn)入環(huán)境”,當(dāng)變量離開執(zhí)行環(huán)境時則標(biāo)記為“離開環(huán)境”,被標(biāo)記為“進(jìn)入環(huán)境”的變量是不能被回收的,因為它們正在被使用,而標(biāo)記為“離開環(huán)境”的變量則可以被回收。

環(huán)境可以理解為我們的執(zhí)行上下文,全局作用域的變量只會在頁面關(guān)閉時才會被銷毀。

// 假設(shè)這里是全局上下文 var b = 1; // b 標(biāo)記進(jìn)入環(huán)境 function func() { var a = 1; return a + b; // 函數(shù)執(zhí)行時,a 被標(biāo)記進(jìn)入環(huán)境 } func(); // 函數(shù)執(zhí)行結(jié)束,a 被標(biāo)記離開環(huán)境,被回收 // 但是 b 沒有標(biāo)記離開環(huán)境 JavaScript 內(nèi)存泄漏的一些場景

JavaScript 的內(nèi)存回收機制雖然能回收絕大部分的垃圾內(nèi)存,但是還是存在回收不了的情況。程序員要讓瀏覽器內(nèi)存泄漏,瀏覽器也是管不了的。

下面有些例子是在執(zhí)行環(huán)境中,沒離開當(dāng)前執(zhí)行環(huán)境,還沒觸發(fā)標(biāo)記清除法。所以你需要讀懂上面 JavaScript 的內(nèi)存回收機制,才能更好的理解下面的場景。

意外的全局變量 // 在全局作用域下定義 function count(num) { a = 1; // a 相當(dāng)于 window.a = 1; return a + num; }

不過在 eslint 幫助下,這種場景現(xiàn)在基本沒人會犯了,eslint 會直接報錯,了解下就好。

遺忘的計時器

無用的計時器忘記清理,是最容易犯的錯誤之一。

拿一個 vue 組件舉個例子。

上面的組件銷毀的時候,setInterval 還是在運行的,里面涉及到的內(nèi)存都是沒法回收的(瀏覽器會認(rèn)為這是必須的內(nèi)存,不是垃圾內(nèi)存),需要在組件銷毀的時候清除計時器。

遺忘的事件監(jiān)聽

無用的事件監(jiān)聽器忘記清理也是最容易犯的錯誤之一。

還是使用 vue 組件舉個例子。

上面的組件銷毀的時候,resize 事件還是在監(jiān)聽中,里面涉及到的內(nèi)存都是沒法回收的,需要在組件銷毀的時候移除相關(guān)的事件。

遺忘的 Set 結(jié)構(gòu)

Set 是 ES6 中新增的數(shù)據(jù)結(jié)構(gòu),如果對 Set 不熟,可以看這里。

如下是有內(nèi)存泄漏的(成員是引用類型,即對象):

let testSet = new Set(); let value = { a: 1 }; testSet.add(value); value = null;

需要改成這樣,才會沒有內(nèi)存泄漏:

let testSet = new Set(); let value = { a: 1 }; testSet.add(value); testSet.delete(value); value = null;

有個更便捷的方式,使用 WeakSet,WeakSet 的成員是弱引用,內(nèi)存回收不會考慮這個引用是否存在。

let testSet = new WeakSet(); let value = { a: 1 }; testSet.add(value); value = null; 遺忘的 Map 結(jié)構(gòu)

Map 是 ES6 中新增的數(shù)據(jù)結(jié)構(gòu),如果對 Map 不熟,可以看這里。

如下是有內(nèi)存泄漏的(成員是引用類型,即對象):

let map = new Map(); let key = [1, 2, 3]; map.set(key, 1); key = null;

需要改成這樣,才會沒有內(nèi)存泄漏:

let map = new Map(); let key = [1, 2, 3]; map.set(key, 1); map.delete(key); key = null;

有個更便捷的方式,使用 WeakMap,WeakMap 的鍵名是弱引用,內(nèi)存回收不會考慮到這個引用是否存在。

let map = new WeakMap(); let key = [1, 2, 3]; map.set(key, 1); key = null 遺忘的訂閱發(fā)布

和上面事件監(jiān)聽器的道理是一樣的。

建設(shè)訂閱發(fā)布事件有三個方法,emit、on、off 三個方法。

還是繼續(xù)使用 vue 組件舉例子:

 

上面組件銷毀的時候,自定義 test 事件還是在監(jiān)聽中,里面涉及到的內(nèi)存都是沒辦法回收的,需要在組件銷毀的時候移除相關(guān)的事件。


遺忘的閉包

閉包是經(jīng)常使用的,閉包能提供很多的便利,

首先看下下面的代碼:

function closure() { const name = '名字'; return () => { return name.split('').reverse().join(''); } } const reverseName = closure(); reverseName(); // 這里調(diào)用了 reverseName

上面有沒有內(nèi)存泄漏?是沒有的,因為 name 變量是要用到的(非垃圾),這也是從側(cè)面反映了閉包的缺點,內(nèi)存占用相對高,數(shù)量多了會影響性能。

但是如果 reverseName 沒有被調(diào)用,在當(dāng)前執(zhí)行環(huán)境未結(jié)束的情況下,嚴(yán)格來說,這樣是有內(nèi)存泄漏的,name 變量是被 closure 返回的函數(shù)調(diào)用了,但是返回的函數(shù)沒被使用,在這個場景下 name 就屬于垃圾內(nèi)存。name 不是必須的,但是還是占用了內(nèi)存,也不可被回收。

當(dāng)然這種也是極端情況,很少人會犯這種低級錯誤。這個例子可以讓我們更清楚的認(rèn)識內(nèi)存泄漏。

DOM 的引用

每個頁面上的 DOM 都是占用內(nèi)存的,建設(shè)有一個頁面 A 元素,我們獲取到了 A 元素 DOM 對象,然后賦值到了一個變量(內(nèi)存指向是一樣的),然后移除了頁面上的 A 元素,如果這個變量由于其他原因沒有被回收,那么就存在內(nèi)存泄漏,如下面的例子:

class Test { constructor() { this.elements = { button: document.querySelector('#button'), div: document.querySelector('#div') } } removeButton() { document.body.removeChild(this.elements.button); // this.elements.button = null } } const test = new Test(); test.removeButton();

上面的例子 button 元素雖然在頁面上移除了,但是內(nèi)存指向換成了 this.elements.button,內(nèi)存占用還是存在的。所以上面的代碼還需要這么寫:this.elements.button = null,手動釋放內(nèi)存。

如何發(fā)現(xiàn)內(nèi)存泄漏

內(nèi)存泄漏時,內(nèi)存一般都是周期性的增長,我們可以借助谷歌瀏覽器的開發(fā)者工具進(jìn)行判斷。

這里針對下面的例子進(jìn)行一步步的的排查和找到問題點:

運行 停止

確實是否是內(nèi)存泄漏問題

訪問上面的代碼頁面,打開開發(fā)者工具,切換至 Performance 選項,勾選 Memory 選項。

在頁面上點擊運行按鈕,然后在開發(fā)者工具上面點擊左上角的錄制按鈕,10 秒后在頁面上點擊停止按鈕,5 秒停止內(nèi)存錄制。得到內(nèi)存走勢如下:


 

由上圖可知,10 秒之前內(nèi)存周期性增長,10 秒后點擊了停止按鈕,內(nèi)存平穩(wěn),不再遞增。我們可以使用內(nèi)存走勢圖判斷是否存在內(nèi)存泄漏。

查找內(nèi)存泄漏的位置

上一步確認(rèn)內(nèi)存泄漏問題后,我們繼續(xù)利用開發(fā)者工具進(jìn)行問題查找。

訪問上面的代碼頁面,打開開發(fā)者工具,切換至 Memory 選項。頁面上點擊運行按鈕,然后點擊開發(fā)者工具左上角的錄制按鈕,錄制完成后繼續(xù)點擊錄制,直到錄制完成三個為止。然后點擊頁面上的停止按鈕,在連續(xù)錄制三次內(nèi)存(不要清理之前的錄制)。


 

從這里也可以看出,點擊運行按鈕之后,內(nèi)存在不斷的遞增。點擊停止按鈕之后,內(nèi)存就平穩(wěn)了。雖然我們也可以用這種方式來判斷是否存在內(nèi)存泄漏,但是沒有第一步的方法便捷,走勢圖也更加直觀。

然后第二步的主要目的是為了記錄 JavaScript 堆內(nèi)存,我們可以看到哪個堆占用的內(nèi)存更高。


 

從內(nèi)存記錄中,發(fā)現(xiàn) array 對象占用最大,展開后發(fā)現(xiàn),第一個 object elements 占用最大,選擇這個 object elements 后可以在下面看到 newArr 變量,然后點擊后面的高亮鏈接,就可以跳轉(zhuǎn)到 newArr 附近。

分享到:
標(biāo)簽:JavaScript
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達(dá)人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定