S自帶一套內存管理引擎,負責創建對象、銷毀對象,以及垃圾回收。這期探討一下垃圾回收機制。垃圾回收機制主要是由一個叫垃圾收集器(garbage collector,簡稱GC)的后臺進程負責監控、清理對象,并及時回收空閑內存。
和C#、Java一樣JavaScript有自動垃圾回收機制,也就是說執行環境會負責管理代碼執行過程中使用的內存,在開發過程中就無需考慮內存分配及無用內存的回收問題了。
而 JavaScript 在創建對象(對象、字符串等)時會為它們分配內存,不再使用時會“自動”釋放內存,這個過程稱為垃圾收集。
內存生命周期中的每一個階段:
分配內存 —? 內存是由操作系統分配的,它允許您的程序使用它。在低級語言(例如 C 語言)中,這是一個開發人員需要自己處理的顯式執行的操作。然而,在高級語言中,系統會自動為你分配內在。
使用內存 — 這是程序實際使用之前分配的內存,在代碼中使用分配的變量時,就會發生讀和寫操作。
釋放內存 — 釋放所有不再使用的內存,使之成為自由內存,并可以被重新利用。與分配內存操作一樣,這一操作在低級語言中也是需要顯式地執行。
JS會在創建變量時自動分配內存,在不使用地時候會自動周期性的釋放內存,釋放的過程就叫 "垃圾回收"。這個機制有好的一面,當然也也有不好的一面。一方面自動分配內存減輕了開發者的負擔,開發者不用過多的去關注內存使用,但是另一方面,正是因為因為是自動回收,所以如果不清楚回收的機制,會很容易造成混亂,而混亂就很容易造成"內存泄漏".由于是自動回收,所以就存在一個 "內存是否需要被回收的" 的問題,但是這個問題的判定在程序中意味著無法通過某個算法去準確完整的解決,后面探討的回收機制只能有限地去解決一般的問題。
垃圾回收對是否需要回收的問題主要依賴于對變量的判定是否可訪問,由此衍生出兩種主要的回收算法:
標記清理是js最常用的回收策略,2012年后所有瀏覽器都使用了這種策略,此后的對回收策略的改進也是基于這個策略的改進。其策略是:
引用計數策略相對而言不常用,因為弊端較多。其思路是對每個值記錄它被引用的次數,通過最后對次數的判斷(引用數為0)來決定是否保留,具體的規則有
最重要的問題就是,循環引用 的問題
function refProblem () {
let a=new Object();
let b=new Object();
a.c=b;
b.c=a; //互相引用
}
復制代碼
根據之前提到的規則,兩個都互相引用了,引用計數不為0,所以兩個變量都無法回收。如果頻繁地調用改函數,則會造成很嚴重的內存泄漏。
V8的回收機制基于 分代回收機制 ,將內存分為新生代(young generation)和老生代(tenured generation),新生代為存活時間較短的對象,老生代為存活時間較長或者常駐內存的變量。
V8將堆分成了幾個不同的區域
Scavenge 算法是新生代空間中的主要算法,該算法由 C.J. Cheney 在 1970 年在論文 A nonrecursive list compacting algorithm 提出。 Scavenge 主要采用了 Cheney算法,Cheney算法新生代空間的堆內存分為2塊同樣大小的空間,稱為 Semi space,處于使用狀態的成為 From空間 ,閑置的稱為 To 空間。垃圾回收過程如下:
之前說過,標記清除策略會產生內存碎片,從而影響內存的使用,這里 標記整理算法(Mark-Compact)的出現就能很好的解決這個問題。標記整理算法是在 標記清除(Mark-Sweep )的基礎上演變而來的,整理算法會將活躍的對象往邊界移動,完成移動后,再清除不活躍的對象。
由于需要移動移動對象,所以在處理速度上,會慢于Mark-Sweep。
為了避免應用邏輯與垃圾回收器看到的邏輯不一樣,垃圾回收器在執行回收時會停止應用邏輯,執行完回收任務后,再繼續執行應用邏輯。這種行為就是 全停頓,停頓的時間取決于不同引擎執行一次垃圾回收的時間。這種停頓對新生代空間的影響較小,但對老生代空間可能會造成停頓的現象。
為了解決全停頓的現象,2011年V8推出了增量標記。V8將標記過程分為一個個的子標記過程,同時讓垃圾回收標記和JS應用邏輯交替進行,直至標記完成。
內存泄漏的問題難以察覺,在函數被調用很多次的情況下,內存泄漏可能是個大問題。常見的內存泄漏主要有下面幾個場景。
function hello (){
name='tom'
}
hello();
復制代碼
未聲明的對象會被綁定在全局對象上,就算不被使用了,也不會被回收,所以寫代碼的時候,一定要記得聲明變量。
let name='Tom';
setInterval(()=> {
console.log(name);
}, 100);
復制代碼
定時器的回調通過閉包引用了外部變量,如果定時器不清除,name會一直占用著內存,所以用定時器的時候最好明白自己需要哪些變量,檢查定時器內部的變量,另外如果不用定時器了,記得及時清除定時器。
let out=function() {
let name='Tom';
return function () {
console.log(name);
}
}
復制代碼
由于閉包會常駐內存,在這個例子中,如果out一直存在,name就一直不會被清理,如果name值很大的時候,就會造成比較嚴重的內存泄漏。所以一定要慎重使用閉包。
mounted() {
window.addEventListener("resize", ()=> {
//do something
});
}
復制代碼
在頁面初始化時綁定了事件監聽,但是在頁面離開的時候未清除事監聽,就會導致內存泄漏。
文章為參考資料總結的筆記文章,我最近在重學js,會將復習總結的文章記錄在Github,有想一起復習的小伙伴可私信一起參與復習總結!
過去,我們瀏覽靜態網站時無須過多關注內存管理,因為加載新頁面時,之前的頁面信息會從內存中刪除。 然而,隨著單頁Web應用(SPA)的興起,應用程序消耗的內存越來越多,這不僅會降低瀏覽器性能,甚至會導致瀏覽器卡死。
因此,在編碼實踐中,開發人員需要更加關注與內存相關的內容。因此,小編今天將為大家介紹JavaScript內存泄漏的編程模式,并提供一些內存管理的改進方法。
JavaScript對象被保存在瀏覽器內存的堆中,并通過引用方式訪問。值得一提的是,JavaScript垃圾回收器則運行于后臺,并通過識別無法訪問的對象來釋放并恢復底層存儲空間,從而保證JavaScript引擎的良好運行狀態。
當內存中的對象在垃圾回收周期中應該被清理時,若它們被另一個仍然存在于內存中的對象通過一個意外的引用所持有,就會引發內存泄漏問題。這種情況下,冗余對象會繼續占據內存空間,導致應用程序消耗過多的內存資源,并可能導致性能下降和表現不佳的情況出現。因此,及時清理無用對象并釋放內存資源是至關重要的,以確保應用程序的正常運行和良好的性能表現。
那么如何知道代碼中是否存在內存泄漏?內存泄漏往往隱蔽且很難檢測和定位。即使代碼中存在內存泄漏,瀏覽器在運行時也不會返回任何錯誤。如果注意到頁面的性能逐漸下降,可以使用瀏覽器內置的工具來確定是否存在內存泄漏以及是哪個對象引起的。
任務管理器(不要與操作系統的任務管理器混淆)提供了瀏覽器中所有選項卡和進程的概覽。Chrome 中,可以通過在 Linux 和 Windows 操作系統上按 Shift+Esc 來打開任務管理器;而在 Firefox 中,通過在地址欄中鍵入 about:performance 則可以訪問內置的管理器,它可以顯示每個標簽的 JavaScript 內存占用情況。如果網站停留在那里什么都不做,但 JavaScript內存使用量逐漸增加,那很可能是存在內存泄漏。
開發者工具提供了一些先進的內存管理方法,例如,使用Chrome瀏覽器的性能記錄工具,可以對頁面的性能進行可視化分析。在這個過程中,可以通過一些指標來判斷是否存在內存泄漏問題,比如堆內存使用量增加的情況,并及時采取措施解決這些問題,以確保應用程序的正常運行和良好的性能表現。
另外,通過Chrome和Firefox的開發者工具提供的內存工具,可以進一步探索內存使用情況。隊列內存使用快照的比較可以顯示在兩個快照之間分配了多少內存以及分配的位置,并提供額外信息來幫助識別代碼中存在問題的對象。這些工具為開發者提供了便利,能夠更好地進行內存管理和性能優化,提高應用程序的質量和性能。
研究內存泄漏問題就相當于尋找符合垃圾回收機制的編程方式,有效避免對象引用的問題。下面小編就為大家介紹幾個常見的容易導致內存泄漏的地方:
全局變量始終存儲在根目錄下,且永遠不會被回收。而在JavaScript的開發中,一些錯誤會導致局部變量被轉換到了全局,尤其是在非嚴格的代碼模式下。下面是兩個常見的局部變量被轉化到全局變量的情況:
function createGlobalVariables() {
leaking1='I leak into the global scope'; // 為未聲明的變量賦值
this.leaking2='I also leak into the global scope'; // 使用this指向全局對象
};
createGlobalVariables();
window.leaking1;
window.leaking2;
注意:嚴格模式("use strict")將幫助您避免上面示例中的內存泄漏和控制臺錯誤。
函數中定義的變量會在函數退出調用棧并且在函數外部沒有指向它的引用時被清除。而閉包則會保持被引用的變量一直存在,即便函數的執行已經終止。
function outer() {
const potentiallyHugeArray=[];
return function inner() {
potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
console.log('Hello');
};
};
const sayHello=outer(); // contains definition of the function inner
function repeat(fn, num) {
for (let i=0; i < num; i++){
fn();
}
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray
// now imagine repeat(sayHello, 100000)
在這個例子中,potentiallyHugeArray從未被任何函數返回,也無法被訪問,但它的大小會隨著調用 inner 方法的次數而增長。
在JavaScript中,使用使用 setTimeout 或 setInterval函數引用對象是防止對象被垃圾回收的最常見方法。當在代碼中設置循環定時器(可以使 setTimeout 表現得像 setInterval,即使其遞歸)時,只要回調可調用,定時器回調對象的引用就會永遠保持活動狀態。
例如下面的這段代碼,只有在移除定時器后,data對象才會被垃圾回收。在沒有移除setInterval之前,它永遠不會被刪除,并且data.hugeString 會一直保留在內存中,直到應用程序停止。
function setCallback() {
const data={
counter: 0,
hugeString: new Array(100000).join('x')
};
return function cb() {
data.counter++; // data object is now part of the callback's scope
console.log(data.counter);
}
}
setInterval(setCallback(), 1000); // how do we stop it?
那么應該如何避免上述這種情況的發生呢?可以從以下兩個方法入手:
如下方的代碼所示:
function setCallback() {
// 'unpacking' the data object
let counter=0;
const hugeString=new Array(100000).join('x'); // gets removed when the setCallback returns
return function cb() {
counter++; // only counter is part of the callback's scope
console.log(counter);
}
}
const timerId=setInterval(setCallback(), 1000); // saving the interval ID
// doing something ...
clearInterval(timerId); // stopping the timer i.e. if button pressed
活動的事件監聽器會阻止其范圍內的所有變量被回收。一旦添加,事件監聽器會一直生效,直到下面兩種情況的發生:
在下面的示例中,使用匿名內聯函數作為事件監聽器,這意味著它不能與 removeEventListener() 一起使用。此外,由于document 不能被移除,觸發方法中的內容會一直駐留內存,即使只使用它觸發一次。
const hugeString=new Array(100000).join('x');
document.addEventListener('keyup', function() { // anonymous inline function - can't remove it
doSomething(hugeString); // hugeString is now forever kept in the callback's scope
});
那么如何避免這種情況呢?可以通過removeEventListener()釋放監聽器:
function listener() {
doSomething(hugeString);
}
document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // ...and here
如果事件監聽器只需要運行一次,addEventListener() 可以帶有第三個參數,一個提供附加選項的對象。只要將 {once: true} 作為第三個參數傳遞給 addEventListener(),監聽器將在事件處理一次后自動刪除。
document.addEventListener('keyup', function listener() {
doSomething(hugeString);
}, {once: true}); // listener will be removed after running once
如果不斷向緩存中添加內容,而未使用的對象也沒有移除,也沒有限制緩存的大小,那么緩存的大小就會無限增長:
let user_1={ name: "Peter", id: 12345 };
let user_2={ name: "Mark", id: 54321 };
const mapCache=new Map();
function cache(obj){
if (!mapCache.has(obj)){
const value=`${obj.name} has an id of ${obj.id}`;
mapCache.set(obj, value);
return [value, 'computed'];
}
return [mapCache.get(obj), 'cached'];
}
cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_1); // ['Peter has an id of 12345', 'cached']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(mapCache); // ((…)=> "Peter has an id of 12345", (…)=> "Mark has an id of 54321")
user_1=null; // removing the inactive user
// Garbage Collector
console.log(mapCache); // ((…)=> "Peter has an id of 12345", (…)=> "Mark has an id of 54321") // first entry is still in cache
為了解決這個問題,需要清除不需要的緩存:
一種有效的解決內存泄漏問題的方法是使用WeakMap。它是一種數據結構,其中鍵引用被保持為弱引用,并且僅接受對象作為鍵。如果使用對象作為鍵,并且它是唯一引用該對象的引用,相關條目將從緩存中移除,并進行垃圾回收。在下面的示例中,當替換user_1后,與之關聯的條目將在下一次垃圾回收時自動從WeakMap中移除。
let user_1={ name: "Peter", id: 12345 };
let user_2={ name: "Mark", id: 54321 };
const weakMapCache=new WeakMap();
function cache(obj){
// ...same as above, but with weakMapCache
return [weakMapCache.get(obj), 'cached'];
}
cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(weakMapCache); // ((…)=> "Peter has an id of 12345", (…)=> "Mark has an id of 54321"}
user_1=null; // removing the inactive user
// Garbage Collector
console.log(weakMapCache); // ((…)=> "Mark has an id of 54321") - first entry gets garbage collected
對于復雜的應用程序,檢測和修復 JavaScript 內存泄漏問題可能是一項非常艱巨的任務。了解內存泄漏的常見原因以防止它們發生是非常重要的。在涉及內存和性能方面,最重要的是用戶體驗,這才是最重要的。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。