溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

避免JavaScript內(nèi)存泄露的方法有哪些

發(fā)布時間:2021-11-17 15:25:43 來源:億速云 閱讀:124 作者:iii 欄目:web開發(fā)

本篇內(nèi)容主要講解“避免JavaScript內(nèi)存泄露的方法有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“避免JavaScript內(nèi)存泄露的方法有哪些”吧!

簡介

內(nèi)存泄露是每個開發(fā)者最終都要面對的問題,它是許多問題的根源:反應遲緩,崩潰,高延遲,以及其他應用問題。

什么是內(nèi)存泄露?

本質(zhì)上,內(nèi)存泄露可以定義為:應用程序不再需要占用內(nèi)存的時候,由于某些原因,內(nèi)存沒有被操作系統(tǒng)或可用內(nèi)存池回收。編程語言管理內(nèi)存的方式各不相 同。只有開發(fā)者最清楚哪些內(nèi)存不需要了,操作系統(tǒng)可以回收。一些編程語言提供了語言特性,可以幫助開發(fā)者做此類事情。另一些則寄希望于開發(fā)者對內(nèi)存是否需 要清晰明了。

JavaScript 內(nèi)存管理

JavaScript  是一種垃圾回收語言。垃圾回收語言通過周期性地檢查先前分配的內(nèi)存是否可達,幫助開發(fā)者管理內(nèi)存。換言之,垃圾回收語言減輕了“內(nèi)存仍可用”及“內(nèi)存仍可 達”的問題。兩者的區(qū)別是微妙而重要的:僅有開發(fā)者了解哪些內(nèi)存在將來仍會使用,而不可達內(nèi)存通過算法確定和標記,適時被操作系統(tǒng)回收。

JavaScript 內(nèi)存泄露

垃圾回收語言的內(nèi)存泄露主因是不需要的引用。理解它之前,還需了解垃圾回收語言如何辨別內(nèi)存的可達與不可達。

Mark-and-sweep

大部分垃圾回收語言用的算法稱之為 Mark-and-sweep 。算法由以下幾步組成:

  1. 垃圾回收器創(chuàng)建了一個“roots”列表。Roots 通常是代碼中全局變量的引用。JavaScript 中,“window” 對象是一個全局變量,被當作 root 。window 對象總是存在,因此垃圾回收器可以檢查它和它的所有子對象是否存在(即不是垃圾);

  2. 所有的 roots 被檢查和標記為激活(即不是垃圾)。所有的子對象也被遞歸地檢查。從 root 開始的所有對象如果是可達的,它就不被當作垃圾。

  3. 所有未被標記的內(nèi)存會被當做垃圾,收集器現(xiàn)在可以釋放內(nèi)存,歸還給操作系統(tǒng)了。

現(xiàn)代的垃圾回收器改良了算法,但是本質(zhì)是相同的:可達內(nèi)存被標記,其余的被當作垃圾回收。

不需要的引用是指開發(fā)者明知內(nèi)存引用不再需要,卻由于某些原因,它仍被留在激活的 root 樹中。在 JavaScript 中,不需要的引用是保留在代碼中的變量,它不再需要,卻指向一塊本該被釋放的內(nèi)存。有些人認為這是開發(fā)者的錯誤。

為了理解 JavaScript 中最常見的內(nèi)存泄露,我們需要了解哪種方式的引用容易被遺忘。

三種類型的常見 JavaScript 內(nèi)存泄露

1:意外的全局變量

JavaScript 處理未定義變量的方式比較寬松:未定義的變量會在全局對象創(chuàng)建一個新變量。在瀏覽器中,全局對象是 window 。

function foo(arg) {     bar = "this is a hidden global variable"; }

真相是:

function foo(arg) {     window.bar = "this is an explicit global variable"; }

函數(shù) foo 內(nèi)部忘記使用 var ,意外創(chuàng)建了一個全局變量。此例泄露了一個簡單的字符串,無傷大雅,但是有更糟的情況。

另一種意外的全局變量可能由 this 創(chuàng)建:

function foo() {     this.variable = "potential accidental global"; }  // Foo 調(diào)用自己,this 指向了全局對象(window) // 而不是 undefined foo();

在 JavaScript 文件頭部加上 'use strict',可以避免此類錯誤發(fā)生。啟用嚴格模式解析 JavaScript ,避免意外的全局變量。

全局變量注意事項

盡管我們討論了一些意外的全局變量,但是仍有一些明確的全局變量產(chǎn)生的垃圾。它們被定義為不可回收(除非定義為空或重新分配)。尤其當全局變量用于 臨時存儲和處理大量信息時,需要多加小心。如果必須使用全局變量存儲大量數(shù)據(jù)時,確保用完以后把它設置為 null  或者重新定義。與全局變量相關的增加內(nèi)存消耗的一個主因是緩存。緩存數(shù)據(jù)是為了重用,緩存必須有一個大小上限才有用。高內(nèi)存消耗導致緩存突破上限,因為緩 存內(nèi)容無法被回收。

2:被遺忘的計時器或回調(diào)函數(shù)

在 JavaScript 中使用 setInterval 非常平常。一段常見的代碼:

var someResource = getData(); setInterval(function() {     var node = document.getElementById('Node');     if(node) {         // 處理 node 和 someResource         node.innerHTML = JSON.stringify(someResource));     } }, 1000);

此例說明了什么:與節(jié)點或數(shù)據(jù)關聯(lián)的計時器不再需要,node 對象可以刪除,整個回調(diào)函數(shù)也不需要了??墒?,計時器回調(diào)函數(shù)仍然沒被回收(計時器停止才會被回收)。同時,someResource 如果存儲了大量的數(shù)據(jù),也是無法被回收的。

對于觀察者的例子,一旦它們不再需要(或者關聯(lián)的對象變成不可達),明確地移除它們非常重要。老的 IE 6 是無法處理循環(huán)引用的。如今,即使沒有明確移除它們,一旦觀察者對象變成不可達,大部分瀏覽器是可以回收觀察者處理函數(shù)的。

觀察者代碼示例:

var element = document.getElementById('button'); function onClick(event) {     element.innerHTML = 'text'; }  element.addEventListener('click', onClick);

對象觀察者和循環(huán)引用注意事項

老版本的 IE 是無法檢測 DOM 節(jié)點與 JavaScript 代碼之間的循環(huán)引用,會導致內(nèi)存泄露。如今,現(xiàn)代的瀏覽器(包括 IE 和  Microsoft Edge)使用了更先進的垃圾回收算法,已經(jīng)可以正確檢測和處理循環(huán)引用了。換言之,回收節(jié)點內(nèi)存時,不必非要調(diào)用 removeEventListener 了。

3:脫離 DOM 的引用

有時,保存 DOM 節(jié)點內(nèi)部數(shù)據(jù)結構很有用。假如你想快速更新表格的幾行內(nèi)容,把每一行 DOM 存成字典(JSON  鍵值對)或者數(shù)組很有意義。此時,同樣的 DOM 元素存在兩個引用:一個在 DOM  樹中,另一個在字典中。將來你決定刪除這些行時,需要把兩個引用都清除。

var elements = {     button: document.getElementById('button'),     image: document.getElementById('image'),     text: document.getElementById('text') };  function doStuff() {     image.src = 'http://some.url/image';     button.click();     console.log(text.innerHTML);     // 更多邏輯 }  function removeButton() {     // 按鈕是 body 的后代元素     document.body.removeChild(document.getElementById('button'));      // 此時,仍舊存在一個全局的 #button 的引用     // elements 字典。button 元素仍舊在內(nèi)存中,不能被 GC 回收。 }

此外還要考慮 DOM 樹內(nèi)部或子節(jié)點的引用問題。假如你的 JavaScript 代碼中保存了表格某一個 <td> 的引用。將來決定刪除整個表格的時候,直覺認為 GC 會回收除了已保存的 <td> 以外的其它節(jié)點。實際情況并非如此:此<td> 是表格的子節(jié)點,子元素與父元素是引用關系。由于代碼保留了 <td> 的引用,導致整個表格仍待在內(nèi)存中。保存 DOM 元素引用的時候,要小心謹慎。

4:閉包

閉包是 JavaScript 開發(fā)的一個關鍵方面:匿名函數(shù)可以訪問父級作用域的變量。

代碼示例:

var theThing = null; var replaceThing = function () {   var originalThing = theThing;   var unused = function () {     if (originalThing)       console.log("hi");   };    theThing = {     longStr: new Array(1000000).join('*'),     someMethod: function () {       console.log(someMessage);     }   }; };  setInterval(replaceThing, 1000);

代碼片段做了一件事情:每次調(diào)用 replaceThing ,theThing 得到一個包含一個大數(shù)組和一個新閉包(someMethod)的新對象。同時,變量 unused 是一個引用 originalThing 的閉包(先前的 replaceThing 又調(diào)用了 theThing )。思緒混亂了嗎?最重要的事情是,閉包的作用域一旦創(chuàng)建,它們有同樣的父級作用域,作用域是共享的。someMethod 可以通過 theThing 使用,someMethod 與 unused 分享閉包作用域,盡管 unused從未使用,它引用的 originalThing 迫使它保留在內(nèi)存中(防止被回收)。當這段代碼反復運行,就會看到內(nèi)存占用不斷上升,垃圾回收器(GC)并無法降低內(nèi)存占用。本質(zhì)上,閉包的鏈表已經(jīng)創(chuàng)建,每一個閉包作用域攜帶一個指向大數(shù)組的間接的引用,造成嚴重的內(nèi)存泄露。

Meteor 的博文 解釋了如何修復此種問題。在 replaceThing 的***添加 originalThing = null 。

Chrome 內(nèi)存剖析工具概覽

Chrome 提供了一套很棒的檢測 JavaScript 內(nèi)存占用的工具。與內(nèi)存相關的兩個重要的工具:timeline 和 profiles。

Timeline

避免JavaScript內(nèi)存泄露的方法有哪些

timeline 可以檢測代碼中不需要的內(nèi)存。在此截圖中,我們可以看到潛在的泄露對象穩(wěn)定的增長,數(shù)據(jù)采集快結束時,內(nèi)存占用明顯高于采集初期,Node(節(jié)點)的總量也很高。種種跡象表明,代碼中存在 DOM 節(jié)點泄露的情況。

Profiles

避免JavaScript內(nèi)存泄露的方法有哪些

Profiles 是你可以花費大量時間關注的工具,它可以保存快照,對比 JavaScript 代碼內(nèi)存使用的不同快照,也可以記錄時間分配。每一次結果包含不同類型的列表,與內(nèi)存泄露相關的有 summary(概要) 列表和 comparison(對照) 列表。

summary(概要) 列表展示了不同類型對象的分配及合計大?。簊hallow size(特定類型的所有對象的總大小),retained  size(shallow size 加上其它與此關聯(lián)的對象大?。?。它還提供了一個概念,一個對象與關聯(lián)的 GC root 的距離。

對比不同的快照的 comparison list 可以發(fā)現(xiàn)內(nèi)存泄露。

實例:使用 Chrome 發(fā)現(xiàn)內(nèi)存泄露

實質(zhì)上有兩種類型的泄露:周期性的內(nèi)存增長導致的泄露,以及偶現(xiàn)的內(nèi)存泄露。顯而易見,周期性的內(nèi)存泄露很容易發(fā)現(xiàn);偶現(xiàn)的泄露比較棘手,一般容易被忽視,偶爾發(fā)生一次可能被認為是優(yōu)化問題,周期性發(fā)生的則被認為是必須解決的 bug。

以 Chrome 文檔中的代碼為例:

var x = [];  function createSomeNodes() {     var div,         i = 100,         frag = document.createDocumentFragment();      for (;i > 0; i--) {         div = document.createElement("div");         div.appendChild(document.createTextNode(i + " - "+ new Date().toTimeString()));         frag.appendChild(div);     }      document.getElementById("nodes").appendChild(frag); }  function grow() {     x.push(new Array(1000000).join('x'));     createSomeNodes();     setTimeout(grow,1000); }

當 grow 執(zhí)行的時候,開始創(chuàng)建 div 節(jié)點并插入到 DOM 中,并且給全局變量分配一個巨大的數(shù)組。通過以上提到的工具可以檢測到內(nèi)存穩(wěn)定上升。

找出周期性增長的內(nèi)存

timeline 標簽擅長做這些。在 Chrome 中打開例子,打開 Dev Tools ,切換到 timeline,勾選 memory 并點擊記錄按鈕,然后點擊頁面上的 The Button 按鈕。過一陣停止記錄看結果:

避免JavaScript內(nèi)存泄露的方法有哪些

兩種跡象顯示出現(xiàn)了內(nèi)存泄露,圖中的 Nodes(綠線)和 JS heap(藍線)。Nodes 穩(wěn)定增長,并未下降,這是個顯著的信號。

JS heap 的內(nèi)存占用也是穩(wěn)定增長。由于垃圾收集器的影響,并不那么容易發(fā)現(xiàn)。圖中顯示內(nèi)存占用忽漲忽跌,實際上每一次下跌之后,JS heap 的大小都比原先大了。換言之,盡管垃圾收集器不斷的收集內(nèi)存,內(nèi)存還是周期性的泄露了。

確定存在內(nèi)存泄露之后,我們找找根源所在。

保存兩個快照

切換到 Chrome Dev Tools 的 profiles 標簽,刷新頁面,等頁面刷新完成之后,點擊 Take Heap Snapshot 保存快照作為基準。而后再次點擊 The Button 按鈕,等數(shù)秒以后,保存第二個快照。

避免JavaScript內(nèi)存泄露的方法有哪些

篩選菜單選擇 Summary,右側選擇 Objects allocated between Snapshot 1 and Snapshot 2,或者篩選菜單選擇 Comparison ,然后可以看到一個對比列表。

此例很容易找到內(nèi)存泄露,看下 (string) 的 Size Delta Constructor,8MB,58個新對象。新對象被分配,但是沒有釋放,占用了8MB。

如果展開 (string) Constructor,會看到許多單獨的內(nèi)存分配。選擇某一個單獨的分配,下面的 retainers 會吸引我們的注意。

避免JavaScript內(nèi)存泄露的方法有哪些

我們已選擇的分配是數(shù)組的一部分,數(shù)組關聯(lián)到 window 對象的 x 變量。這里展示了從巨大對象到無法回收的 root(window)的完整路徑。我們已經(jīng)找到了潛在的泄露以及它的出處。

我們的例子還算簡單,只泄露了少量的 DOM 節(jié)點,利用以上提到的快照很容易發(fā)現(xiàn)。對于更大型的網(wǎng)站,Chrome 還提供了 Record Heap Allocations 功能。

Record heap allocations 找內(nèi)存泄露

回到 Chrome Dev Tools 的 profiles 標簽,點擊 Record Heap Allocations。工具運行的時候,注意頂部的藍條,代表了內(nèi)存分配,每一秒有大量的內(nèi)存分配。運行幾秒以后停止。

避免JavaScript內(nèi)存泄露的方法有哪些

上圖中可以看到工具的殺手锏:選擇某一條時間線,可以看到這個時間段的內(nèi)存分配情況。盡可能選擇接近峰值的時間線,下面的列表僅顯示了三種 constructor:其一是泄露最嚴重的(string),下一個是關聯(lián)的 DOM 分配,***一個是 Text constructor(DOM 葉子節(jié)點包含的文本)。

從列表中選擇一個 HTMLDivElement constructor,然后選擇 Allocation stack。

避免JavaScript內(nèi)存泄露的方法有哪些

現(xiàn)在知道元素被分配到哪里了吧(grow -> createSomeNodes),仔細觀察一下圖中的時間線,發(fā)現(xiàn) HTMLDivElement constructor 調(diào)用了許多次,意味著內(nèi)存一直被占用,無法被 GC 回收,我們知道了這些對象被分配的確切位置(createSomeNodes)?;氐酱a本身,探討下如何修復內(nèi)存泄露吧。

另一個有用的特性

在 heap allocations 的結果區(qū)域,選擇 Allocation。

避免JavaScript內(nèi)存泄露的方法有哪些

這個視圖呈現(xiàn)了內(nèi)存分配相關的功能列表,我們立刻看到了 grow 和 createSomeNodes。當選擇 grow 時,看看相關的 object constructor,清楚地看到 (string)HTMLDivElement 和 Text 泄露了。

到此,相信大家對“避免JavaScript內(nèi)存泄露的方法有哪些”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關內(nèi)容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI