Chorme 瀏覽器中的垃圾回收和記憶體泄漏
垃圾回收
通常情況下,垃圾資料回收分為手動回收和自動回收兩種策略,
手動回收策略,何時分配記憶體、何時銷毀記憶體都是由代碼控制的,
自動回收策略,產生的垃圾資料是由垃圾回收器來釋放的,并不需要手動通過代碼來釋放,
JavaScript 中呼叫堆疊中的資料回收
JavaScript 引擎會通過向下移動 ESP(記錄當前執行狀態的指標) 來銷毀該函式保存在堆疊中的執行背景關系,
JavaScript 堆中的資料回收
在 V8 中會把堆分為新生代和老生代兩個區域,新生代中存放的是生存時間短的物件,老生代中存放的生存時間久的物件,
新生區通常只支持 1~8M 的容量,而老生區支持的容量就大很多了,對于這兩塊區域,V8 分別使用兩個不同的垃圾回收器,以便更高效地實施垃圾回收,
- 副垃圾回收器,主要負責新生代的垃圾回收,
- 主垃圾回收器,主要負責老生代的垃圾回收,
不論什么型別的垃圾回收器,它們都有一套共同的執行流程,
- 第一步是標記空間中活動物件和非活動物件,所謂活動物件就是還在使用的物件,非活動物件就是可以進行垃圾回收的物件,
- 第二步是回收非活動物件所占據的記憶體,其實就是在所有的標記完成之后,統一清理記憶體中所有被標記為可回收的物件,
- 第三步是做記憶體整理,一般來說,頻繁回收物件后,記憶體中就會存在大量不連續空間,我們把這些不連續的記憶體空間稱為
記憶體碎片,,當記憶體中出現了大量的記憶體碎片之后,如果需要分配較大連續記憶體的時候,就有可能出現記憶體不足的情況,所以最后一步需要整理這些記憶體碎片,(這步其實是可選的,因為有的垃圾回收器不會產生記憶體碎片).
新生代中垃圾回收
新生代中用Scavenge 演算法來處理,把新生代空間對半劃分為兩個區域,一半是物件區域,一半是空閑區域,新加入的物件都會存放到物件區域,當物件區域快被寫滿時,就需要執行一次垃圾清理操作,
在垃圾回收程序中,首先要對物件區域中的垃圾做標記;標記完成之后,就進入垃圾清理階段,副垃圾回收器會把這些存活的物件復制到空閑區域中,同時它還會把這些物件有序地排列起來,所以這個復制程序,也就相當于完成了記憶體整理操作,復制后空閑區域就沒有記憶體碎片了,
完成復制后,物件區域與空閑區域進行角色翻轉,也就是原來的物件區域變成空閑區域,原來的空閑區域變成了物件區域,這樣就完成了垃圾物件的回收操作,同時這種角色翻轉的操作還能讓新生代中的這兩塊區域無限重復使用下去.
為了執行效率,一般新生區的空間會被設定得比較小,也正是因為新生區的空間不大,所以很容易被存活的物件裝滿整個區域,為了解決這個問題,JavaScript 引擎采用了物件晉升策略,也就是經過兩次垃圾回收依然還存活的物件,會被移動到老生區中,
老生代中的垃圾回收
老生代中用標記 - 清除(Mark-Sweep)的演算法來處理,首先是標記程序階段,標記階段就是從一組根元素開始,遞回遍歷這組根元素(遍歷呼叫堆疊),在這個遍歷程序中,能到達的元素稱為活動物件,沒有到達的元素就可以判斷為垃圾資料.然后在遍歷程序中標記,標記完成后就進行清除程序,它和副垃圾回收器的垃圾清除程序完全不同,這個的清楚程序是洗掉標記資料,
清除演算法后,會產生大量不連續的記憶體碎片,而碎片過多會導致大物件無法分配到足夠的連續記憶體,于是又產生了標記 - 整理(Mark-Compact)演算法,這個標記程序仍然與標記 - 清除演算法里的是一樣的,但后續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然后直接清理掉端邊界以外的記憶體,從而讓存活物件占用連續的記憶體塊,
全停頓
由于 JavaScript 是運行在主執行緒之上的,一旦執行垃圾回收演算法,都需要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢后再恢復腳本執行,我們把這種行為叫做全停頓,
在 V8 新生代的垃圾回收中,因其空間較小,且存活物件較少,所以全停頓的影響不大,但老生代就不一樣了,如果執行垃圾回收的程序中,占用主執行緒時間過久,主執行緒是不能做其他事情的,比如頁面正在執行一個 JavaScript 影片,因為垃圾回收器在作業,就會導致這個影片在垃圾回收程序中無法執行,這將會造成頁面的卡頓現象,
為了降低老生代的垃圾回收而造成的卡頓,V8 將標記程序分為一個個的子標記程序,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,我們把這個演算法稱為增量標記(Incremental Marking)演算法.
使用增量標記演算法,可以把一個完整的垃圾回收任務拆分為很多小的任務,這些小的任務執行時間比較短,可以穿插在其他的 JavaScript 任務中間執行,這樣當執行上述影片效果時,就不會讓用戶因為垃圾回收任務而感受到頁面的卡頓了,
記憶體泄漏
不再用到的記憶體,沒有及時釋放,就叫做記憶體泄漏(memory leak),
記憶體泄漏發生的原因
- 快取
有時候為了方便資料的快捷復用,我們會使用快取,但是快取必須有一個大小上限才有用,高記憶體消耗將會導致快取突破上限,因為快取內容無法被回收,
-
佇列消費不及時
當瀏覽器佇列消費不及時時,會導致一些作用域變數得不到及時的釋放,因而導致記憶體泄漏, -
全域變數
除了常規設定了比較大的物件在全域變數中,還可能是意外導致的全域變數,如:
function foo(arg) {
bar = "this is a hidden global variable";
}
在函式中,沒有使用 var/let/const 定義變數,這樣實際上是定義在window上面,變成了window.bar,
再比如由于this導致的全域變數:
function foo() {
this.bar = "this is a hidden global variable";
}
foo()
這種函式,在window作用域下被呼叫時,函式里面的this指向了window,執行時實際上為window.bar=xxx,這樣也產生了全域變數,
- 計時器中參考沒有清除
先看如下代碼:
var someData = https://www.cnblogs.com/LuckyWinty/p/getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someData));
}
}, 1000);
這里定義了一個計時器,每隔1s把一些資料寫到Node節點里面,但是當這個Node節點被洗掉后,這里的邏輯其實都不需要了,可是這樣寫,卻導致了計時器里面的回呼函式無法被回收,同時,someData里的資料也是無法被回收的,
- 閉包
看以下這個閉包:
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);
每次呼叫 replaceThing ,theThing 會創建一個大陣列和一個新閉包(someMethod)的新物件,同時,變數 unused 是一個參考 originalThing(theThing) 的閉包,閉包的作用域一旦創建,它們有同樣的父級作用域,作用域是共享的,
即 someMethod 可以通過 theThing 使用,someMethod 與 unused 分享閉包作用域,盡管 unused 從未使用,它參考的 originalThing 迫使它保留在記憶體中(防止被回收),
因此,當這段代碼反復運行,就會看到記憶體占用不斷上升,垃圾回收器(GC)并無法降低記憶體占用,
本質上,閉包的鏈表已經創建,每一個閉包作用域攜帶一個指向大陣列的間接的參考,造成嚴重的記憶體泄漏,
- 事件監聽
例如,Node.js 中 Agent 的 keepAlive 為 true 時,可能造成的記憶體泄漏,當 Agent keepAlive 為 true 的時候,將會復用之前使用過的 socket,如果在 socket 上添加事件監聽,忘記清除的話,因為 socket 的復用,將導致事件重復監聽從而產生記憶體泄漏,
記憶體泄漏的識別方法
- 使用 Chrome 任務管理器實時監視記憶體使用
打開 chrome 瀏覽器,點擊右上角主選單,選擇更多工具->任務管理器,這樣就開啟了任務管理器面板,然后再右鍵點擊任務管理器的表格標題并啟用 JavaScript使用的記憶體,能看到這樣的面板:
下面兩列可以告訴您與頁面的記憶體使用有關的不同資訊:

記憶體占用空間(Memory)串列示原生記憶體,DOM 節點存盤在原生記憶體中, 如果此值正在增大,則說明正在創建 DOM 節點,JavaScript使用的記憶體(JavaScript Memory)串列示 JS 堆,此列包含兩個值, 您感興趣的值是實時數字(括號中的數字),實時數字表示您的頁面上的可到達物件正在使用的記憶體量, 如果此數字在增大,要么是正在創建新物件,要么是現有物件正在增長,
當你頁面穩定下來之后,這兩個的值還在上漲,你就可以查一查是否記憶體泄漏了,
- 利用chrome 時間軸記錄可視化記憶體泄漏
Performance(時間軸)能夠面板直觀實時顯示JS記憶體使用情況、節點數量、監聽器數量等,
打開 chrome 瀏覽器,調出除錯面板(DevTools),點擊Performance選項(低版本是Timeline),勾選Memory復選框,一種比較好的做法是使用強制垃圾回收開始和結束記錄,在記錄時點擊 Collect garbage 按鈕 (強制垃圾回收按鈕) 可以強制進行垃圾回收,
所以錄制順序可以這樣:開始錄制前先點擊垃圾回收-->點擊開始錄制-->點擊垃圾回收-->點擊結束錄制,
面板介紹如圖:

錄制結果如圖:

首先,從圖中我們可以看出不同顏色的曲線代表的含義,這里主要關注JS堆記憶體、節點數量、監聽器數量,滑鼠移到曲線上,可以在左下角顯示具體資料,在實際使用程序中,如果您看到這種 JS 堆大小或節點大小不斷增大的模式,則可能存在記憶體泄漏,
- 使用堆快照發現已分離 DOM 樹的記憶體泄漏
只有頁面的 DOM 樹或 JavaScript 代碼不再參考 DOM 節點時,DOM 節點才會被作為垃圾進行回收, 如果某個節點已從 DOM 樹移除,但某些 JavaScript 仍然參考它,我們稱此節點為“已分離”,已分離的 DOM 節點是記憶體泄漏的常見原因,
同理,調出除錯面板,點擊Memory,然后選擇Heap Snapshot,然后點擊進行錄制,錄制完成后,選中錄制結果,在 Class filter 文本框中鍵入 Detached,搜索已分離的 DOM 樹,
以這段代碼為例:
<html>
<head>
</head>
<body>
<button id="createBtn">增加節點</button>
<script>
var detachedNodes;
function create() {
var ul = document.createElement('ul');
for (var i = 0; i < 10; i++) {
var li = document.createElement('li');
ul.appendChild(li);
}
detachedTree = ul;
}
document.getElementById('createBtn').addEventListener('click', create);
</script>
</body>
</html>
點擊幾下,然后記錄,可以得到以下資訊:

舊版的面板,還會有顏色標注,黃色的物件實體表示它被JS代碼參考,紅色的物件實體表示被黃色節點參考的游離節點,上圖是新版本的,不會有顏色標識,但是還是可以一個個來看,如上圖,點開節點,可以看到下面的參考資訊,上面可以看出,有個HTMLUListElement(ul節點)被window.detachedNodes參考,再結合代碼,原來是沒有加var/let/const宣告,導致其成了全域變數,所以DOM無法釋放,
- 按函式調查記憶體分配
打開面板,點擊JavaScript Profiler,如果沒看到這個選項,你可以點除錯面板右上角的三個點,選擇more tools,然后選擇,
ps: chrome 舊版的瀏覽器,這個功能在 Profiles 里面,點Record Allocation Profile即可.
操作步驟:點start->在頁面進行你要檢測的操作->點stop,

DevTools 按函式顯示記憶體分配明細,默認視圖為 Heavy (Bottom Up),將分配了最多記憶體的函式顯示在最上方,還有函式的位置,你可以看看是哪些函式占用記憶體較多,
避免記憶體泄漏的方法
- 少用全域變數,避免意外產生全域變數
- 使用閉包要及時注意,有Dom元素的參考要及時清理,
- 計時器里的回呼沒用的時候要記得銷毀,
- 為了避免疏忽導致的遺忘,我們可以使用
WeakSet和WeakMap結構,它們對于值的參考都是不計入垃圾回識訓制的,表示這是弱參考,
舉個例子:
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
這種情況下,一旦消除對該節點的參考,它占用的記憶體就會被垃圾回識訓制釋放,Weakmap 保存的這個鍵值對,也會自動消失,
基本上,如果你要往物件上添加資料,又不想干擾垃圾回識訓制,就可以使用 WeakMap,
參考資料
- 極客時間《瀏覽器作業原理與實踐》
- https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/
- https://developers.google.com/web/tools/chrome-devtools/memory-problems?hl=zh-cn
最后
- 歡迎加我微信(winty230),拉你進技術群,長期交流學習...
- 歡迎關注「前端Q」,認真學前端,做個有專業的技術人...

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/179481.html
標籤:JavaScript
上一篇:Javascript定時器
