无法读取内存属于错误吗_深入了解 JavaScript 内存泄露
用戶一般不會在一個 Web 頁面停留比較久,即使有一點內存泄漏,重載頁面內存也會跟著釋放。而且瀏覽器也有自動回收內存的機制,所以我們前端其實并沒有像 C、C++ 這類語言一樣,特別關注內存泄漏的問題。
但是如果我們對內存泄漏沒有什么概念,有時候還是有可能因為內存泄漏,導致頁面卡頓。了解內存泄漏,如何避免內存泄漏,也是我們提升前端技能的必經之路。
俗話說好記憶不如爛筆頭,所以本人就總結了一些內存泄漏相關的知識,避免一些低級的內存泄漏問題。
什么是內存?
在硬件級別上,計算機內存由大量觸發器組成。每個觸發器包含幾個晶體管,能夠存儲一個位。單個觸發器可以通過唯一標識符尋址,因此我們可以讀取和覆蓋它們。因此,從概念上講,我們可以把我們的整個計算機內存看作是一個巨大的位數組,我們可以讀和寫。這么底層的概念,了解下就好,絕大多數數情況下,JavaScript 語言作為你們高級語言,無需我們使用二進制進直接進行讀和寫。
內存生命周期
內存也是有生命周期的,不管什么程序語言,一般可以按順序分為三個周期:
- 分配期分配你所需要的內存
- 使用期使用分配到的內存(讀、寫)
- 釋放期不需要時將其釋放和歸還
內存分配 -> 內存使用 -> 內存釋放。
什么是內存泄漏?
在計算機科學中,內存泄漏指由于疏忽或錯誤造成程序未能釋放已經不再使用的內存。內存泄漏并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費。如果內存不需要時,沒有經過生命周期的釋放期,那么就存在內存泄漏。
內存泄漏簡單理解:無用的內存還在占用,得不到釋放和歸還。比較嚴重時,無用的內存會持續遞增,從而導致整個系統卡頓,甚至崩潰。
JavaScript 內存管理機制
像 C 語言這樣的底層語言一般都有底層的內存管理接口,比如 malloc()和free()。相反,JavaScript是在創建變量(對象,字符串等)時自動進行了分配內存,并且在不使用它們時“自動”釋放。 釋放的過程稱為垃圾回收。這個“自動”是混亂的根源,并讓JavaScript(和其他高級語言)開發者錯誤的感覺他們可以不關心內存管理。JavaScript 內存管理機制和內存的生命周期是一一對應的。首先需要分配內存,然后使用內存,最后釋放內存。
其中 JavaScript 語言不需要程序員手動分配內存,絕大部分情況下也不需要手動釋放內存,對 JavaScript 程序員來說通常就是使用內存(即使用變量、函數、對象等)。
內存分配
JavaScript 定義變量就會自動分配內存的。我們只需了解 JavaScript 的內存是自動分配的就足夠了。
看下內存自動分配的例子:
// 給數值變量分配內存 let number = 123; // 給字符串分配內存 const string = "xianshannan"; // 給對象及其包含的值分配內存 const object = {a: 1,b: null }; // 給數組及其包含的值分配內存(就像對象一樣) const array = [1, null, "abra"]; // 給函數(可調用的對象)分配內存 function func(a){return a; }內存使用
使用值的過程實際上是對分配內存進行讀取與寫入的操作。讀取與寫入可能是寫入一個變量或者一個對象的屬性值,甚至傳遞函數的參數。根據上面的內存自動分配例子,我們繼續內存使用的例子:
// 寫入內存 number = 234; // 讀取 number 和 func 的內存,寫入 func 參數內存 func(number);內存回收
前端界一般稱垃圾內存回收為 GC(Garbage Collection,即垃圾回收)。
內存泄漏一般都是發生在這一步,JavaScript 的內存回收機制雖然能回收絕大部分的垃圾內存,但是還是存在回收不了的情況,如果存在這些情況,需要我們手動清理內存。
以前一些老版本的瀏覽器的 JavaScript 回收機制沒那么完善,經常出現一些 bug 的內存泄漏,不過現在的瀏覽器基本都沒這些問題了,已過時的知識這里就不做深究了。
這里了解下現在的 JavaScript 的垃圾內存的兩種回收方式,熟悉下這兩種算法可以幫助我們理解一些內存泄漏的場景。
- 引用計數垃圾收集
這是最初級的垃圾收集算法。此算法把“對象是否不再需要”簡化定義為“對象有沒有其他對象引用到它”。如果沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。
看下下面的例子,“這個對象”的內存被回收了嗎?
// “這個對象”分配給 a 變量 var a = { a: 1, b: 2, } // b 引用“這個對象” var b = a; // 現在,“這個對象”的原始引用 a 被 b 替換了 a = 1;
當前執行環境中,內存還沒有被回收的,只有加上下面這個才會被回收:
b = 1;
這樣引用的"這個對象"的內存才會被回收。 - 標記清除法
當變量進入執行環境時標記為“進入環境”,當變量離開執行環境時則標記為“離開環境”,被標記為“進入環境”的變量是不能被回收的,因為它們正在被使用,而標記為“離開環境”的變量則可以被回收
環境可以理解為我們的作用域,但是全局作用域的變量只會在頁面關閉才會銷毀。
// 假設這里是全局變量 // b 被標記進入環境 var b = 2; function test() { var a = 1; // 函數執行時,a 被標記進入環境 return a + b; } // 函數執行結束,a 被標記離開環境,被回收 // 但是 b 就沒有被標記離開環境 test();
JavaScript 內存泄漏的一些場景
JavaScript 的內存回收機制雖然能回收絕大部分的垃圾內存,但是還是存在回收不了的情況。程序員要讓瀏覽器內存泄漏,瀏覽器也是管不了的。
意外的全局變量
// 在全局作用域下定義 function count(number) {// basicCount 相當于 window.basicCount = 2;basicCount = 2;return basicCount + number; }不過 eslint 幫助下,這種場景現在基本沒人會犯了,eslint 會直接報錯,了解下就好。
被遺忘的計時器
無用的計時器忘記清理是新手最容易犯的錯誤之一。
就拿一個 vue 組件來做例子。
<template><div></div> </template><script> export default {methods: {refresh() {// 獲取一些數據},},mounted() {setInterval(function() {// 輪詢獲取數據this.refresh()}, 2000)}, } </script>上面的組件銷毀的時候,setInterval 還是在運行的,里面涉及到的內存都是沒法回收的(瀏覽器會認為這是必須的內存,不是垃圾內存),需要在組件銷毀的時候清除計時器,如下:
<template><div></div> </template><script> export default {methods: {refresh() {// 獲取一些數據},},mounted() {this.refreshInterval = setInterval(function() {// 輪詢獲取數據this.refresh()}, 2000)},beforeDestroy() {clearInterval(this.refreshInterval)}, } </script>被遺忘的事件監聽器
無用的事件監聽器忘記清理是新手最容易犯的錯誤之一。
還是繼續使用 vue 組件做例子。
<template><div></div> </template><script> export default {mounted() {window.addEventListener('resize', () => {// 這里做一些操作})}, } </script>上面的組件銷毀的時候,resize 事件還是在監聽中,里面涉及到的內存都是沒法回收的(瀏覽器會認為這是必須的內存,不是垃圾內存),需要在組件銷毀的時候移除相關的事件,如下:
<template><div></div> </template><script> export default {mounted() {this.resizeEventCallback = () => {// 這里做一些操作}window.addEventListener('resize', this.resizeEventCallback)},beforeDestroy() {window.removeEventListener('resize', this.resizeEventCallback)}, } </script>閉包
閉包是經常使用的,閉包能給我們帶來很多便利。
首先看下這個代碼:
function closure() {const name = 'xianshannan'return () => {return name.split('').reverse().join('')} } const reverseName = closure() // 這里調用了 reverseName reverseName();上面有沒有內存泄漏?
上面是沒有內存泄漏的,name 變量是要用到的(非垃圾),name 內存無法回收。這也是從側面反映了閉包的缺點,內存占用相對高,量多了會有性能影響。
但是改成這樣就是有內存泄漏的:
function closure() {const name = 'xianshannan'return () => {return name.split('').reverse().join('')} } closure()嚴格來說,這樣是有內存泄漏的,name 變量是被 closure 返回的函數調用了,但是返回的函數沒被使用,這個場景下 name 就屬于垃圾內存,但是它還是占用了內存,也不可被回收。
當然這種也是極端情況,很少人會犯這種低級錯誤。這個例子可以讓我們更清楚的認識內存泄漏。
脫離 DOM 的引用
每個頁面上的 DOM 都是占用內存的,假設有一個頁面 A 元素,我們獲取到了 A 元素 DOM 對象,然后賦值到了一個變量(內存指向是一樣的),然后移除了頁面的 A 元素,如果這個變量由于其他原因沒有被回收,那么就存在內存泄漏,如下面的例子:
class Test {constructor() {this.elements = {button: document.querySelector('#button'),div: document.querySelector('#div'),span: document.querySelector('#span'),}}removeButton() {document.body.removeChild(this.elements.button)// this.elements.button = null} }const a = Test() a.removeButton()上面的例子 button 元素 雖然在頁面上移除了,但是內存指向換為了 this.elements.button,內存占用還是存在的。所以上面的代碼還需要這樣寫: this.elements.button = null,手動釋放這個內存。
如何發現內存泄漏?
內存泄漏時,內存一般都是會周期性的增長,我們可以借助谷歌瀏覽器的開發者工具進行判別。
這里不進行詳細的開發者工具使用說明,詳細看谷歌開發者工具,不過谷歌瀏覽器是不斷迭代更新的,有些文檔落后了,界面長得不一樣。
本人測試的谷歌版本為:版本 76.0.3809.100(正式版本) (64 位)。
這里針對下面例子進行一步一步的排查和找到問題出現在哪里:
<!DOCTYPE html> <html><head><meta charset="utf-8" /></head><body><div id="app"><button id="run">運行</button><button id="stop">停止</button></div><script>const arr = []for (let i = 0; i < 200000; i++) {arr.push(i)}let newArr = []function run() {newArr = newArr.concat(arr)}let clearRundocument.querySelector('#run').onclick = function() {clearRun = setInterval(() => {run()}, 1000)}document.querySelector('#stop').onclick = function() {clearInterval(clearRun)}</script></body> </html>上面例子的代碼可以直接運行的,怎么運行我就不多說了。
第一步:確定是否是內存泄漏問題
訪問上面的代碼頁面,打開谷歌開發者工具,切換至 Performance 選項,勾選 Memory 選項。
在頁面上點擊運行按鈕,然后在開發者工具上面點擊左上角的錄制按鈕,10 秒后在頁面上點擊停止按鈕,5 秒后停止內存錄制。得到的內存走勢如下:
由上圖可知,10 秒之前內存周期性增長,10 后點擊了停止按鈕,內存平穩,不在遞增。
我們可以使用內存走勢圖判斷當前頁面是否有內存泄漏。經過測試上面的代碼 20000 個數組項改為 20 個數組項,內存走勢也一樣能看出來。
第二步:查找內存泄漏出現的位置
上一步確認是內存泄漏問題后,我們繼續利用谷歌開發者工具進行問題查找。
訪問上面的代碼頁面,打開谷歌開發者工具,切換至 Memory 選項。頁面上點擊運行按鈕,然后點擊開發者工具左上角錄制按鈕,錄制完成后繼續點擊錄制,知道錄制完三個為止。然后點擊頁面的停止按鈕,再連續錄制 3 次內存(不要清理之前的錄制)。下圖就是進行這些步驟后的截圖:
從這里也可以看出,點擊運行按鈕后,內存在不斷遞增。點擊停止按鈕后,內存就平穩了。雖然我們也可以使用這樣的方式來判別是否存在內存泄漏,但是不夠第一步的方法便捷,走勢圖更直接。
然后第二步的主要目的來了,記錄 JavaScript 堆內存才是內存錄制的主要目的,我們可以看到那個堆占用了內存更高。
在剛才的錄制中選擇 Snapshot 3 ,然后按照 Shallow Size 進行逆序排序(不了解的可以看內存術語),如下:
從內存記錄中,發現 array 對象占用最大,展開后發現,第一個 object elements 占用最大,選擇這個 object elements后可以在下面看到 newArr 變量,然后點擊 test:23,只要是高亮下劃線的地方都可以進去看看。 (測試頁面時 test.html),可以跳轉到 newArr 附近。
參考資料
- 維基百科-內存泄漏
- 內存管理
- 4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
- javascript 垃圾回收機制
- How JavaScript works: memory management + how to handle 4 common memory leaks
來源:思否
作者:格西南
原文:https://segmentfault.com/a/1190000020231307
總結
以上是生活随笔為你收集整理的无法读取内存属于错误吗_深入了解 JavaScript 内存泄露的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python获取链接跳转后地址_爬虫:获
- 下一篇: springboot 定时器_基于Spr