摘要:這是因為我們訪問了數組中不存在的數組元素它超過了最后一個實際分配到內存的數組元素字節(jié),并且有可能會讀取或者覆寫的位。包含個元素的新數組由和數組元素所組成中的內存使用中使用分配的內存主要指的是內存讀寫。
原文請查閱這里,本文有進行刪減,文后增了些經驗總結。
本系列持續(xù)更新中,Github 地址請查閱這里。
這是 JavaScript 工作原理的第三章。
我們將會討論日常使用中另一個被開發(fā)者越來越忽略的重要話題,這都是日益成熟和復雜的編程語言的鍋,即內存管理問題。我們將會提供在創(chuàng)建 SessionStack 的時候所遵循的處理 JavaScript 內存泄漏的幾條小技巧,因為我們需要保證 SessionStack 不會引起內存泄漏或者不會增加我們所集成的 web 應用程序的內存消耗。
概述像 C 語言擁有底層的內存管理原語如 malloc() 和 free()。開發(fā)者使用這些原語來顯式從操作系統(tǒng)分配和釋放內存。
與此同時,當創(chuàng)建事物(對象,字符串等)的時候,JavaScript 分配內存并且當它們不再使用的時候 "自動釋放" 內存,這一過程稱為內存垃圾回收。這個乍看起來本質上是 "自動化釋放內存" 的釋放資源是引起混亂的原因,并且給予 JavaScript(及其它高級語言)開發(fā)者一個錯誤的印象即他們可以選擇忽略內存管理。這是一個巨大的錯誤。
即使是當使用高級語言的時候,開發(fā)者也應該要理解內存管理(或者至少是一些基礎)。有時候自動化內存管理會存在一些問題(比如垃圾回收中的 bugs 或者實施的局限性等等),為了能夠合理地處理內存泄漏問題(或者以最小代價和代碼缺陷來尋找一個合適的方案),開發(fā)者就必須理解內存管理。
內存生命周期不管你使用哪種編程語言,內存生命周期幾乎是一樣的:
以下是每一步生命周期所發(fā)生事情的一個概述:
分配內存-內存是由操作系統(tǒng)分配,這樣程序就可以使用它。在底層語言(例如 C 語言),開發(fā)者可以顯式地操作內存。而在高級語言中,操作系統(tǒng)幫你處理。
使用內存-這是程序實際使用之前分配的內存的階段。當你在代碼中使用已分配的變量的時候,就會發(fā)生內存讀寫的操作。
釋放內存-該階段你可以釋放你不再使用的整塊內存,該內存就可以被釋放且可以被再利用。和內存分配操作一樣,該操作也是用底層語言顯式編寫的。
為快速瀏覽調用堆棧和動態(tài)內存管理的概念,你可以閱讀第一篇文章。
啥是內存?在直接跳向 JavaScript 內存管理之前,先來簡要地介紹一下內存及其工作原理。
從硬件層面看,計算機內存是由大量的 flip flops 所組成的(這里大概查了下,即大量的二進制電路所組成的)。每個 flip flop 包含少量晶體管并能夠存儲一個比特位。單個的 flip flops 可以通過一個唯一標識符尋址,所以就可以讀和覆寫它們。因此,理論上,我們可以把整個計算機內存看成是由一個巨大的比特位數組所組成的,這樣就可以進行讀和寫。
作為猿類,我們并不擅長用位來進行所有的邏輯思考和計算,所以我們把位組織成一個更大的組,這樣就可以用來表示數字。8 位稱為一字節(jié)。除了字節(jié)還有字(16 或 32 位)。
內存中存儲著很多東西:
所有變量及所有程序使用的其它數據。
程序代碼,包括操作系統(tǒng)的代碼。
編譯器和操作系統(tǒng)一起協(xié)作來為你進行內存管理,但是建議你了解一下底層是如何實現(xiàn)的。
當編譯代碼的時候,編譯器會檢查原始數據類型并提前計算出程序運行所需要的內存大小。在所謂的靜態(tài)堆棧空間中,所需的內存大小會被分配給程序。這些變量所分配到的內存所在的空間之所以被稱為靜態(tài)內存空間是因為當調用函數的時候,函數所需的內存會被添加到現(xiàn)存內存的頂部。當函數中斷,它們被以 LIFO(后進先出) 的順序移出內存。比如,考慮如下代碼:
int n; // 4 字節(jié) int x[4]; // 4 個元素的數組,每個數組元素 4 個字節(jié) double m; // 8 字節(jié)
編譯器會立即計算出代碼所需的內存:4 + 4 x 4 + 8 = 28 字節(jié)。
編譯器是這樣處理當前整數和浮點數的大小的。大約 20 年前,整數一般是 2 字節(jié)而 浮點數是 4 字節(jié)。代碼不用依賴于當前基礎數據類型的字節(jié)大小。
編譯器會插入標記,標記會和操作系統(tǒng)協(xié)商從堆棧中獲取所需要的內存大小,以便在堆棧中存儲變量。
在以上示例中,編譯知道每個變量的準確內存地址。事實上,當你編寫變量 n 的時候,會在內部把它轉換為類似 "內存地址 412763" 的樣子。
注意到這里當我們試圖訪問 x[4] 時候,將會訪問到 m 相關的數據。這是因為我們訪問了數組中不存在的數組元素-它超過了最后一個實際分配到內存的數組元素 x[3] 4 字節(jié),并且有可能會讀取(或者覆寫) m 的位。這幾乎可以確定會產生其它程序所預料不到的后果。
當函數調用其它函數的時候,各個函數都會在被調用的時候取得其在堆棧中的各自分片內存地址。函數會把保存它所有的本地變量,但也會有一個程序計數器用來記住函數在其執(zhí)行環(huán)境中的地址。當函數運行結束時,其內存塊可以再次被用作其它用途。
動態(tài)內存分配不幸的是,想要知道編譯時一個變量需要多少內存并沒有想象中那般容易。設想一下若要做類似如下事情:
int n = readInput(); // 從用戶讀取信息 ... // 創(chuàng)建一個含有 n 個元素的數組
這里,編譯器并不知道編譯時數組需要多少內存,因為這是由用戶輸入的數組元素的值所決定的。
因此,就不能夠在堆棧中為變量分配內存空間。相反,程序需要在運行時顯式地從操作系統(tǒng)分配到正確的內存空間。這里的內存是由動態(tài)內存空間所分配的。靜態(tài)和動態(tài)內存分配的差異總結如下圖表:
為了完全理解動態(tài)內存分配的工作原理,我們需要花點時間了解指針,這個就可能有點跑題了 ^.^。如果你對指針感興趣,請留言,然后我們將會在以后的章節(jié)中討論更多關于指針的內容。
JavaScript 中的內存分配現(xiàn)在,我們將會介紹在 JavaScript 中是如何分配內存的((第一步)。
JavaScript 通過聲明變量值,自己處理內存分配工作而不需要開發(fā)者干涉。
var n = 374; // 為數字分配內存 var s = "sessionstack"; // 為字符串分配內存 var o = { a: 1, b: null }; // 為對象及其值分配內存 var a = [1, null, "str"]; // (類似對象)為數組及其數組元素值分配內存 function f(a) { return a + 3; } // 分配一個函數(一個可調用對象) // 函數表達式也分配一個對象 someElement.addEventListener("click", function() { someElement.style.backgroundColor = "blue"; }, false);
一些函數調用也會分配一個對象:
var d = new Date(); // 分配一個日期對象 var e = document.createElement("div"); // 分配一個 DOM 元素
可以分配值或對象的方法:
var s1 = "sessionstack"; var s2 = s1.substr(0, 3); // s2 為一個新字符串 // 因為字符串是不可變的,所以 JavaScript 可能會選擇不分配內存而只是存儲數組 [0, 3] 的內存地址范圍。 var a1 = ["str1", "str2"]; var a2 = ["str3", "str4"]; var a3 = a1.concat(a2); // 包含 4 個元素的新數組由 a1 和 a2 數組元素所組成JavaScript 中的內存使用
JavaScript 中使用分配的內存主要指的是內存讀寫。
可以通過為變量或者對象屬性賦值,亦或是為函數傳參來使用內存。
釋放不再使用的內存大多數的內存管理問題是出現(xiàn)在這一階段。
痛點在于檢測出何時分配的內存是閑置的。它經常會要求開發(fā)者來決定程序中的這段內存是否已經不再使用,然后釋放它。
高級程序語言集成了一塊稱為垃圾回收器的軟件,該軟件的工作就是追蹤內存分配和使用情況以便找出并自動釋放閑置的分配內存片段。
不幸的是,這是個近似的過程,因為判定一些內存片段是否閑置的普遍問題在于其不可判定性(不能為算法所解決)。
大多數的垃圾回收器會收集那些不再被訪問的內存,比如引用該內存的所有變量超出了內存尋址范圍。然而還是會有低于近似值的內存空間被收集,因為在任何情況下仍然可能會有變量在內存尋址范圍內引用該內存地址,即使該內存是閑置的。
內存垃圾回收由于找出 "不再使用" 的內存的不可判定性,針對這一普遍問題,垃圾回收實現(xiàn)了一個有限的解決方案。本小節(jié)將會闡述必要的觀點來理解主要的內存垃圾回收算法及其局限性。
內存引用引用是內存垃圾回收算法所依賴的主要概念之一。
在內存管理上下文中,如果對象 A 訪問了另一個對象 B 表示 A 引用了對象 B(可以隱式或顯式)。舉個栗子,一個 JavaScript 對象有引用了它的原型(隱式引用)和它的屬性值(顯式引用)。
在這個上下文中,"對象" 的概念被拓展超過了一般的 JavaScript 對象并且包含函數作用域(或者全局詞法作用域)。
詞法作用域定義了如何在嵌套函數中解析變量名。即使父函數已經返回,內部的函數仍然會包含父函數的作用域。垃圾回收引用計數
這是最簡單的內存垃圾回收算法。當一個對象被 0 引用,會被標記為 "可回收內存垃圾"。
看下如下代碼:
var o1 = { o2: { x: 1 } }; // 創(chuàng)建兩個對象。 // "o1" 引用對象 "o2" 作為其屬性。全部都是不可回收的。 // "o3" 是第二個引用 "o1" 對象的變量 var o3 = o1; o1 = 1; // 現(xiàn)在,原先在 "o1" 中的對象只有一個單一的引用,以變量 "o3" 來表示 // 引用對象的 "o2" 屬性。 // 該對象有兩個引用:一個是作為屬性,另一個是 "o4" 變量 var o4 = o3.o2; // "o1" 對象現(xiàn)在只有 0 引用,它可以被作為內存垃圾回收。 // 然而,其 "o2" 屬性仍然被變量 "o4" 所引用,所以它的內存不能夠被釋放。 o3 = "374"; o4 = null; // "o1" 中的 "o2" 屬性現(xiàn)在只有 0 引用了。所以 "o1" 對象可以被回收。循環(huán)引用是個麻煩事
循環(huán)引用會造成限制。在以下的示例中,創(chuàng)建了兩個互相引用的對象,這樣就會造成循環(huán)引用。函數調用之后他們將會超出范圍,所以,實際上它們是無用且可以釋放對他們的引用。然而,引用計數算法會認為由于兩個對象都至少互相引用一次,所以他們都不可回收的。
function f() { var o1 = {}; var o2 = {}; o1.P = O2; // O1 引用 o2 o2.p = o1; // o2 引用 o1. 這就造成循環(huán)引用 } f();標記-清除算法
為了判斷是否需要釋放對對象的引用,算法會確定該對象是否可獲得。
標記-清除算法包含三個步驟:
根:一般來說,根指的是代碼中引用的全局變量。就拿 JavaScript 來說,window 對象即是根的全局變量。Node.js 中相對應的變量為 "global"。垃圾回收器會構建出一份所有根變量的完整列表。
隨后,算法會檢測所有的根變量及他們的后代變量并標記它們?yōu)榧せ顮顟B(tài)(表示它們不可回收)。任何根變量所到達不了的變量(或者對象等等)都會被標記為內存垃圾。
最后,垃圾回收器會釋放所有非激活狀態(tài)的內存片段然后返還給操作系統(tǒng)。
標記-清除算法的動態(tài)圖示
該算法比之前的算法要好,因為對象零引用可以讓對象不可獲得。反之則不然,正如之前所看到的循環(huán)引用。
從 2012 年起,所有的現(xiàn)代瀏覽器都內置了一個標記-清除垃圾回收器。前些年所有對于 JavaScript 內存垃圾收集(分代/增量/并發(fā)/并行 垃圾收集)的優(yōu)化都是針對標記-清除算法的實現(xiàn)的優(yōu)化,但既沒有提升垃圾收集算法本身,也沒有提升判定對象是否可獲得的能力。
你可以查看這篇文章 來了解追蹤內存垃圾回收的詳情及包含優(yōu)化了的標記-清除算法。
循環(huán)引用不再讓人蛋疼在之前的第一個示例中,當函數返回,全局對象不再引用這兩個對象。結果,內存垃圾回收器發(fā)現(xiàn)它們是不可獲得的。
即使兩個對象互相引用,也不能夠從根變量獲得他們。
內存垃圾回收器的反直觀行為雖然內存垃圾回收器很方便,但是它們也有其一系列的代價。其中之一便是不確定性。意思即內存垃圾回收具有不可預見性。你不能確定內存垃圾收集的確切時機。這意味著在某些情況下,程序會使用比實際需要更多的內存。在其它情況下,在特定的交互敏感的程序中,你也許需要注意那些內存垃圾收集短暫停時間。雖然不確定性意味著不能夠確定什么時候可以進行內存垃圾收集,但是大多數 GC 的實現(xiàn)都是在內存分配期間進行內存垃圾回收的一般模式。如果沒有進行內存分配,大多數的內存垃圾回收就會保持閑置狀態(tài)??紤]以下情況:
分配一段固定大小的內存。
大多數的元素(或所有)被標記為不可獲得(假設我們賦值我們不再需要的緩存為 null )
不再分配其它內存。
在該情況下,大多數的內存垃圾回收器不會再運行任何的內存垃圾回收。換句話說,即使可以對該不可獲得的引用進行垃圾回收,但是內存收集器不會進行標記。雖然這不是嚴格意義上的內存泄漏,但是這會導致高于平常的內存使用率。
內存泄漏是啥?正如內存管理所說的那樣,內存泄漏即一些程序在過去時使用但處于閑置狀態(tài),卻沒有返回給操作系統(tǒng)或者可用的內存池。
編程語言喜歡多種內存管理方法。然而,某個內存片段是否被使用是一個不確定的問題。換句話說,只有開發(fā)人員清楚某個內存片段是否可以返回給操作系統(tǒng)。
某些編程語言會為開發(fā)者提供功能函數來解決這個問題。其它的編程語言完全依賴于開發(fā)者全權掌控哪個內存片段是可回收的。維其百科上有關于手動和自動內存管理的好文章。
四種常見的 JavaScript 內存泄漏 1: 全局變量JavaScript 以一種有趣的方式來處理未聲明變量:當引用一個未聲明的變量,會在全局對象上創(chuàng)建一個新的變量。在瀏覽器中,全局對象是 window,這意味著如下代碼:
function foo(arg) { bar = "some text"; }
等同于:
function foo(arg) { window.bar = "some text"; }
變量 bar 本意是只能在 foo 函數中被引用。但是如果你沒有用 var 來聲明變量,那么將會創(chuàng)建一個多余的全局變量。在上面的例子中,并不會造成大的事故。但你可以很自然地想象一個更具破壞性的場景。
你也可以使用 this 關鍵字不經意地創(chuàng)建一個全局變量。
function foo() { this.var1 = "potential accidental global"; } // 調用 foo 函數自身,this 會指向全局對象(window)而不是未定義
你可以通過在 JavaScript 文件的頂部添加 "use strict" 來避免以上的所有問題,"use strict" 會切換到更加嚴格的 JavaScript 解析模式,這樣就可以防止創(chuàng)建意外的全局變量。
意外的全局變量的確是個問題,而代碼經常會被顯式定義的全局變量所污染,根據定義這些全局變量是不會被內存垃圾回收器所收集的。你需要特別注意的是使用全局變量來臨時存儲和處理大型的位信息。只有在必要的時候使用全局變量來存儲數據,記得一旦你不再使用的時候,把它賦值為 null 或者對其再分配。
2:定時器及被遺忘的回調函數因為經常在 JavaScript 中使用 setInterval,所以讓我們以它為例。
框架中提供了觀察者和接受回調的其它指令通常會確保當他們的實例不可獲得的時候,所有對回調的引用都會變成不可獲得。很容易找到如下代碼:
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById("renderer"); if (renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); // 這將會每隔大約 5 秒鐘執(zhí)行一次
以上代碼片段展示了使用定時器來引用不再需要的節(jié)點或數據的后果。
renderer 對象會在某些時候被替換或移除,這樣就會導致由定時處理程序封裝的代碼變得冗余。當這種情況發(fā)生的時候,不管是定時處理程序還是它的依賴都不會被垃圾回收,這是由于需要先停止定時器(記住,定時器仍然處于激活狀態(tài))。這可以歸結為保存和處理數據加載的 serverData 變量也不會被垃圾回收。
當使用觀察者的時候,你需要確保一旦你不再需要它們的時候顯式地移除它們(不再需要觀察者或者對象變得不可獲得)。
幸運的是,大多數現(xiàn)代瀏覽器都會替你進行處理:當被觀察者對象變得不可獲得時,即使你忘記移除事件監(jiān)聽函數,瀏覽器也會自動回收觀察者處理程序。以前,一些老掉牙的瀏覽器處理不了這些情況(如老舊的 IE6)。
那么,最佳實踐是當對象被廢棄的時候,移除觀察者處理程序。查看如下例子:
var element = document.getElementById("launch-button"); var counter = 0; function onClick(event) { counter++; element.innerHTML = "text" + counter; } element.addEventListener("click", onClick); // Do stuff element.removeEventListener("click", onClick); element.parentNode.removeChild(element); // 現(xiàn)在當元素超出范圍 // 即使在不能很好處理循環(huán)引用的瀏覽器中也會回收元素和 onClick 事件
在讓一個 DOM 節(jié)點不可獲得之前,你不再需要調用 removeEventListener,因為現(xiàn)代瀏覽器支持用內存垃圾回收器來檢測并適當地處理 DOM 節(jié)點的生命周期。
如果你使用 jQuery API(其它的庫和框架也支持的 API),你可以在廢棄節(jié)點之前移除事件監(jiān)聽函數。jQuery 也會確保即使在老舊的瀏覽器之中,也不會產生內存泄漏。
閉包閉包是 JavaScript 的一個重要功能:嵌套函數可以訪問外部(封閉)函數的變量。鑒于 JavaScript 運行時的實現(xiàn)細節(jié),以下方法可能會造成內存泄漏:
var theThing = null var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // 引用 originalThing console.log("hi"); }; theThing = { longStr: new Array(1000000).join("*"), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
當調用 replaceThing 的時候,theThing 對象由一個大數組和新的閉包(someMethod)所組成。而 originalThing 由 unused 變量創(chuàng)建的閉包所引用(即引用 replaceThing 函數之前的 theThing 變量)。需要記住的是當一旦為同一個父作用域中的閉包創(chuàng)建閉包作用域的時候,該閉包作用域是共享的。
在這樣的情況下,閉包 someMethod 和 unused 共享相同的作用域。unused 引用了 origintalThing。即使 unused 永不使用,也可以在 replaceThing 的作用域外使用 someMethod 函數。然后由于 someMethod 和 unused 共享相同的閉包作用域,unused 變量引用 originalThing 會強迫 unused 保持激活狀態(tài)(兩個閉包共享作用域)。這會阻止內存垃圾回收。
在以上例子中,閉包 someMethod 和 unused 共享作用域,而 unused 引用 origintalThing??梢栽?replaceThing 作用域外通過 theThing 使用 someMethod,即使 unused 從未被使用。事實上,由于 someMethod 和 unused 共享閉包作用域,unused 引用 origintalThing 要求 unused 保持激活狀態(tài)。
所有的這些行為會導致內存泄漏。當你不斷地運行如上代碼片段,你將會發(fā)現(xiàn)飆升的內存使用率。當內存垃圾回收器運行的時候,這些內存使用率不會下降。這里會創(chuàng)建出一份閉包鏈表(當前情況下,其根變量是 theThing),每個閉包作用域都間接引用了大數組。
該問題是由 Metor 小組發(fā)現(xiàn)的并且他們寫了一篇很好的文章來詳細描述該問題。
4: 源自 DOM 引用有時候,開發(fā)者會在數據結構中存儲 DOM 節(jié)點。
假設你想要快速更新幾行表格內容。如果你在一個字典或者數組中保存對每個表格行的引用,這將會造成重復引用相同的 DOM 元素:一個在 DOM 樹中而另一個在字典中。如果你想要釋放對這些表格行的引用,你需要記得讓這些引用變成不可獲得。
var elements = { button: document.getElementById("button"), image: document.getElementById("image") }; function doStuff() { elements.image.src = "http://example.com/image_name.png"; } function removeImage() { // image 元素是 body 元素的直系后代元素 document.body.removeChild(document.getElementById("image")); // 這時,我們仍然在 elements 全局對象中引用了 #button 元素 // 換句話說,按鈕元素仍然在內存中且不能夠被垃圾回收器收集 }
你還需要額外考慮的情況是引用 DOM 樹中的內節(jié)點或者葉節(jié)點。如果你在代碼中保存著對一個單元格的引用,這時候當你決定從 DOM 中移除表格,卻仍然會保持對該單元格的引用,這就會導致大量的內存泄漏。你可以認為內存垃圾回收器將會釋放除了該單元格以外的內存。而這還沒完。因為單元格是表格的一個后代元素而后代元素保存著對其父節(jié)點的引用,對一個單元格的引用會導致無法釋放整個表格所占用的內存。
內存管理心得以下內容為個人原創(chuàng)分享。By 三月。
指導思想盡可能減少內存占用,盡可能減少 GC。
減少 GC 次數
瀏覽器會不定時回收垃圾內存,稱為 GC,不定時觸發(fā),一般在向瀏覽器申請新內存時,瀏覽器會檢測是否到達一個臨界值再進行觸發(fā)。一般來說,GC 會較為耗時,GC 觸發(fā)時可能會導致頁面卡頓及丟幀。故我們要盡可能避免GC的觸發(fā)。GC 無法通過代碼觸發(fā),但部分瀏覽器如 Chrome,可在 DevTools -> TimeLine 頁面手動點擊 CollectGarbage 按鈕觸發(fā) GC。
減少內存占用
降低內存占用,可避免內存占用過多導致的應用/系統(tǒng)卡頓,App 閃退等,在移動端尤為明顯。當內存消耗較多時,瀏覽器可能會頻繁觸發(fā) GC。而如前所述,GC 發(fā)生在申請新內存時,若能避免申請新內存,則可避免GC 觸發(fā)。
優(yōu)化方案 使用對象池對象池(英語:object pool pattern)是一種設計模式。一個對象池包含一組已經初始化過且可以使用的對象,而可以在有需求時創(chuàng)建和銷毀對象。池的用戶可以從池子中取得對象,對其進行操作處理,并在不需要時歸還給池子而非直接銷毀它。這是一種特殊的工廠對象。若初始化、實例化的代價高,且有需求需要經常實例化,但每次實例化的數量較少的情況下,使用對象池可以獲得顯著的效能提升。從池子中取得對象的時間是可預測的,但新建一個實例所需的時間是不確定。
以上摘自維基百科。
使用對象池技術能顯著優(yōu)化需頻繁創(chuàng)建對象時的內存消耗,但建議按不同使用場景做以下細微優(yōu)化。
按需創(chuàng)建
默認創(chuàng)建空對象池,按需創(chuàng)建對象,用完歸還池子。
預創(chuàng)建對象
如在高頻操作下,如滾動事件、TouchMove事件、resize事件、for 循環(huán)內部等頻繁創(chuàng)建對象,則可能會觸發(fā)GC的發(fā)生。故在特殊情況下,可優(yōu)化為提前創(chuàng)建對象放入池子。
高頻情況下,建議使用截流/防抖及任務隊列相關技術。
定時釋放
對象池內的對象不會被垃圾回收,若極端情況下創(chuàng)建了大量對象回收進池子卻不釋放只會適得其反。
故池子需設計定時/定量釋放對象機制,如以已用容量/最大容量/池子使用時間等參數來定時釋放對象。
其他優(yōu)化tips盡可能避免創(chuàng)建對象,非必要情況下避免調用會創(chuàng)建對象的方法,如 Array.slice、Array.map、Array.filter、字符串相加、$("div")、ArrayBuffer.slice 等。
不再使用的對象,手動賦為 null,可避免循環(huán)引用等問題。
使用 Weakmap
生產環(huán)境勿用 console.log 大對象,包括 DOM、大數組、ImageData、ArrayBuffer 等。因為 console.log 的對象不會被垃圾回收。詳見Will console.log prevent garbage collection?。
合理設計頁面,按需創(chuàng)建對象/渲染頁面/加載圖片等。
避免如下問題:
為了省事兒,一次性請求全部數據。
為了省事兒,一次性渲染全部數據,再做隱藏。
為了省事兒,一次性加載/渲染全部圖片。
使用重復 DOM 等,如重復使用同一個彈窗而非創(chuàng)建多個。
如 Vue-Element 框架中,PopOver/Tooltip 等組件用于表格內時會創(chuàng)建 m * n 個實例,可優(yōu)化為只創(chuàng)建一個實例,動態(tài)設置位置及數據。
ImageData 對象是 JS 內存殺手,避免重復創(chuàng)建 ImageData 對象。
重復使用 ArrayBuffer。
壓縮圖片、按需加載圖片、按需渲染圖片,使用恰當的圖片尺寸、圖片格式,如 WebP 格式。
圖片處理優(yōu)化假設渲染一張 100KB 大小,300x500 的透明圖片,粗略的可分為三個過程:
加載圖片
加載圖片二進制格式到內存中并緩存,此時消耗了100KB 內存 & 100KB 緩存。
解碼圖片
將二進制格式解碼為像素格式,此時占用寬 高 24(透明為32位)比特大小的內存,即 300 500 32,約等于 585 KB,這里約定名為像素格式內存。個人猜測此時瀏覽器會回收加載圖片時創(chuàng)建的 100KB 內存。
渲染圖片
通過 CPU 或 GPU 渲染圖片,若為 GPU 渲染,則還需上傳到 GPU 顯存,該過程較為耗時,由圖片尺寸 / 顯存位寬決定,圖片尺寸越大,上傳時間越慢,占用顯存越多。
其中,較舊的瀏覽器如Firefox回收像素內存時機較晚,若渲染了大量圖片時會內存占用過高。
PS:瀏覽器會復用同一份圖片二進制內存及像素格式內存,瀏覽器渲染圖片會按以下順序去獲取數據:
顯存 >> 像素格式內存 >> 二進制內存 >> 緩存 >> 從服務器獲取。我們需控制和優(yōu)化的是二進制內存及像素內存的大小及回收。
總結一下,瀏覽器渲染圖片時所消耗內存由圖片文件大小內存、寬高、透明度等所決定,故建議:
使用 CSS3、SVG、IconFont、Canvas 替代圖片。展示大量圖片的頁面,建議使用 Canvas 渲染而非直接使用img標簽。具體詳見 Javascript的Image對象、圖像渲染與瀏覽器內存兩三事。
適當壓縮圖片,可減小帶寬消耗及圖片內存占用。
使用恰當的圖片尺寸,即響應式圖片,為不同終端輸出不同尺寸圖片,勿使用原圖縮小代替 ICON 等,比如一些圖片服務如 OSS。
使用恰當的圖片格式,如使用WebP格式等。詳細圖片格式對比,使用場景等建議查看web前端圖片極限優(yōu)化策略。
按需加載及按需渲染圖片。
預加載圖片時,切記要將 img 對象賦為 null,否則會導致圖片內存無法釋放。
當實際渲染圖片時,瀏覽器會從緩存中再次讀取。
將離屏 img 對象賦為 null,src 賦為 null,督促瀏覽器及時回收內存及像素格式內存。
將非可視區(qū)域圖片移除,需要時再次渲染。和按需渲染結合時實現(xiàn)很簡單,切換 src 與 v-src 即可。
參考鏈接:garbage-collector-friendly-code/
移動 WEB 通用優(yōu)化策略介紹(二)
H5前端性能優(yōu)化高級進階
Javascript的Image對象、圖像渲染與瀏覽器內存兩三事
web前端圖片極限優(yōu)化策略
MDN Weakmap
函數節(jié)流、函數防抖實現(xiàn)原理分析
a-tour-of-v8-garbage-collection
本系列持續(xù)更新中,Github 地址請查閱這里。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/94842.html
摘要:是如何工作的內存管理以及如何處理四種常見的內存泄漏原文譯者幾個禮拜之前我們開始一系列對于以及其本質工作原理的深入挖掘我們認為通過了解的構建方式以及它們是如何共同合作的,你就能夠寫出更好的代碼以及應用。 JavaScript是如何工作的:內存管理以及如何處理四種常見的內存泄漏 原文:How JavaScript works: memory management + how to han...
摘要:本文作為第三篇,將會討論另一個開發(fā)者容易忽視的重要主題內存管理。我們也會提供一些關于如何處理內存泄露的技巧。這是當前整型和雙精度的大小。然而,這是一組可以收集的內存空間的近似值。 本文轉載自:眾成翻譯譯者:Leslie Wang審校: 為之漫筆鏈接:http://www.zcfy.cc/article/4211原文:https://blog.sessionstack.com/how-j...
摘要:本系列的第一篇文章簡單介紹了引擎運行時間和堆棧的調用。編譯器將插入與操作系統(tǒng)交互的代碼,并申請存儲變量所需的堆棧字節(jié)數。當函數調用其他函數時,每個函數在調用堆棧時獲得自己的塊。因此,它不能為堆棧上的變量分配空間。 本系列的第一篇文章簡單介紹了引擎、運行時間和堆棧的調用。第二篇文章研究了谷歌V8 JavaScript引擎的內部機制,并介紹了一些編寫JavaScript代碼的技巧。 在這第...
閱讀 1686·2021-11-19 09:40
閱讀 2939·2021-09-24 10:27
閱讀 3227·2021-09-02 15:15
閱讀 1888·2019-08-30 15:54
閱讀 1213·2019-08-30 15:54
閱讀 1377·2019-08-30 13:12
閱讀 642·2019-08-28 18:05
閱讀 2808·2019-08-27 10:53