一、內(nèi)存泄露
1、定義
不再用到的內(nèi)存,沒(méi)有及時(shí)釋放,就叫做內(nèi)存泄漏(memory leak)
2、常見(jiàn)的內(nèi)存泄露場(chǎng)景
2.1 隱式全局變量
function foo(arg) {
bar = "暴露到全局的變量";
}
function foo() {
this.variable = "默認(rèn)指向全局";
}
foo();
問(wèn)題:定義全局變量,或者指向全局變量,且沒(méi)有做刪除該全局變量處理,不會(huì)被垃圾回收
解決:
- 不要定義全局變量
- 'use strict' 嚴(yán)格模式,調(diào)用全局this的變量,能將錯(cuò)誤報(bào)出。
2.2 忘記關(guān)閉定時(shí)器
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
問(wèn)題:node引用被定時(shí)器使用,node無(wú)法自動(dòng)被垃圾回收。
解決:用完定時(shí)器后,關(guān)閉定時(shí)器
2.3 忘記關(guān)閉事件回調(diào)
var element = document.getElementById('button');
function onClick(event) {
element.innerText = 'text';
}
element.addEventListener('click', onClick);
問(wèn)題:element引用被事件回調(diào)函數(shù)使用,element無(wú)法自動(dòng)被垃圾回收。
解決:移除節(jié)點(diǎn)之前應(yīng)該先移除節(jié)點(diǎn)身上的事件監(jiān)聽(tīng)器,因?yàn)镮E6沒(méi)處理DOM節(jié)點(diǎn)和JS之間的循環(huán)引用(因?yàn)锽OM和DOM對(duì)象的GC策略都是引用計(jì)數(shù)),可能會(huì)出現(xiàn)內(nèi)存泄漏,現(xiàn)代瀏覽器已經(jīng)不需要這么做了,如果節(jié)點(diǎn)無(wú)法再被訪問(wèn)的話,監(jiān)聽(tīng)器會(huì)被回收掉
2.4 忘記釋放游離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() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// 即使我們移除了button,但因?yàn)闆](méi)有釋放他的引用,所以仍然可以使用elements.button來(lái)操作,并且不會(huì)被垃圾回收
}
var select = document.querySelector;
var treeRef = select("#tree");
var leafRef = select("#leaf");
var body = select("body");
body.removeChild(treeRef);
//#tree不會(huì)被垃圾回收
treeRef = null;
//即使treeRef已經(jīng)刪除,但因?yàn)閘eafRef仍然存在,所以#tree仍然不會(huì)被垃圾回收
leafRef = null;
//現(xiàn)在#tree 和 #leaf都以被垃圾回收
游離子樹(shù)上任意一個(gè)節(jié)點(diǎn)引用沒(méi)有釋放的話,整棵子樹(shù)都無(wú)法釋放,因?yàn)橥ㄟ^(guò)一個(gè)節(jié)點(diǎn)就能找到(訪問(wèn))其它所有節(jié)點(diǎn),都給標(biāo)記上活躍,不會(huì)被清除
我的理解
<button id="tree">
<span id="leaf">leaf</span>
</button>
var treeRef = document.querySelector("#tree");
var leafRef = document.querySelector("#leaf");
var body = document.querySelector("body");
body.removeChild(treeRef);
treeRef = null;
// 即使#tree已經(jīng)被移除且引用也置為空,但因?yàn)閘eaf的存在,內(nèi)存中必然會(huì)保存整個(gè)tree的DOM樹(shù)
console.log(leafRef.parentNode)
// 正常顯示#tree
- 如果沒(méi)有treeRef,leafRef的引用,移除tree,leaf節(jié)點(diǎn),就會(huì)直接移除,且不會(huì)保留在內(nèi)存
- 如果有treeRef,leafRef的引用,把tree的引用和節(jié)點(diǎn)移除,但leafRef的引用仍然會(huì)導(dǎo)致通過(guò)他就能找到(訪問(wèn))其它所有節(jié)點(diǎn),都給標(biāo)記上活躍,不會(huì)被清除
- 就是說(shuō),tree下,如果有任何leaf沒(méi)有解除引用占用,則這棵tree的DOM節(jié)點(diǎn)對(duì)象無(wú)法被垃圾回收
解決方法:當(dāng)決定要移除某個(gè)DOM時(shí),他以及所有子節(jié)點(diǎn)的引用都要釋放掉,比如:
body.removeChild(treeRef);
treeRef=null;
leafRef=null;
2.5 閉包導(dǎo)致
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);
執(zhí)行結(jié)果:內(nèi)存以10M左右的速度一直增加,增加到400M之后,瀏覽器端會(huì)回到12M,再持續(xù)增加。
原因:originalThing 一直在被保存到內(nèi)存,而不是覆蓋存。unused形成的閉包會(huì)保存originalThing,也一直是復(fù)制保存,而不是覆蓋存。
解釋:因?yàn)殚]包的典型實(shí)現(xiàn)方式是每個(gè)函數(shù)對(duì)象都有一個(gè)指向字典對(duì)象的關(guān)聯(lián),這個(gè)字典對(duì)象表示它的詞法作用域。如果定義在replaceThing里的函數(shù)都實(shí)際使用了originalThing,那就有必要保證讓它們都取到同樣的對(duì)象,即使originalThing被一遍遍地重新賦值,所以這些(定義在replaceThing里的)函數(shù)都共享相同的詞法環(huán)境。但V8已經(jīng)聰明到把不會(huì)被任何閉包用到的變量從詞法環(huán)境中去掉了,所以如果把unused刪掉(或者把unused里的originalThing訪問(wèn)去掉),就能解決內(nèi)存泄漏。只要變量被任何一個(gè)閉包使用了,就會(huì)被添到詞法環(huán)境中,被該作用域下所有閉包共享。這是閉包引發(fā)內(nèi)存泄漏的關(guān)鍵
解決:把unused閉包刪除,內(nèi)存占用從400M下降置200M,再把originalThing刪除,內(nèi)存從200M下降到正常。
二. 內(nèi)存膨脹
內(nèi)存膨脹是說(shuō)占用內(nèi)存太多了,chrome是400M就會(huì)處理一次
三、頻繁GC
頻繁GC很影響體驗(yàn)(頁(yè)面暫停的感覺(jué),因?yàn)镾top-The-World),可以通過(guò)Task Manager內(nèi)存大小數(shù)值或者Performance趨勢(shì)折線來(lái)看:
- Task Manager中如果內(nèi)存或JS使用的內(nèi)存數(shù)值頻繁上升下降,就表示頻繁GC
- 趨勢(shì)折線中,如果JS堆大小或者節(jié)點(diǎn)數(shù)量頻繁上升下降,表示存在頻繁GC
解決方案:可以通過(guò)優(yōu)化存儲(chǔ)結(jié)構(gòu)(避免造大量的細(xì)粒度小對(duì)象)、緩存復(fù)用(比如用享元工廠來(lái)實(shí)現(xiàn)復(fù)用)等方式來(lái)解決頻繁GC問(wèn)題
四、Chrome控制臺(tái)中的一些術(shù)語(yǔ)概念
1. Mark-and-sweep
JS相關(guān)的GC算法主要是引用計(jì)數(shù)(IE的BOM、DOM對(duì)象)和標(biāo)記清除(主流做法),各有優(yōu)劣:
引用計(jì)數(shù)回收及時(shí)(引用數(shù)為0立即釋放掉),但循環(huán)引用就永遠(yuǎn)無(wú)法釋放
標(biāo)記清除不存在循環(huán)引用的問(wèn)題(不可訪問(wèn)就回收掉),但回收不及時(shí)需要Stop-The-World
標(biāo)記清除算法步驟如下:
GC維護(hù)一個(gè)root列表,root通常是代碼中持有引用的全局變量。JS中,window對(duì)象就是一例作為root的全局變量。window對(duì)象一直存在,所以GC認(rèn)為它及其所有孩子一直存在(非垃圾)
所有root都會(huì)被檢查并標(biāo)記為活躍(非垃圾),其所有孩子也被遞歸檢查。能通過(guò)root訪問(wèn)到的所有東西都不會(huì)被當(dāng)做垃圾
所有沒(méi)被標(biāo)記為活躍的內(nèi)存塊都被當(dāng)做垃圾,GC可以把它們釋放掉歸還給操作系統(tǒng)
現(xiàn)代GC技術(shù)對(duì)這個(gè)算法做了各種改進(jìn),但本質(zhì)都一樣:可訪問(wèn)的內(nèi)存塊被這樣標(biāo)記出來(lái)后,剩下的就是垃圾
2. Shallow Size
對(duì)象自身占用內(nèi)存的大小,比如字符串和數(shù)組
3. Retained Size
對(duì)象自身及依賴它的對(duì)象(從GC root無(wú)法再訪問(wèn)到的對(duì)象)被刪掉后釋放的內(nèi)存大小
五、Chrome控制臺(tái)性能優(yōu)化調(diào)試
- Performance 性能
主要看內(nèi)存的變化情況,若一直上升,則有內(nèi)存泄露的問(wèn)題
5.png
-
Memory 內(nèi)存
主要是看內(nèi)存中存儲(chǔ)什么東西
比如數(shù)組,對(duì)象,函數(shù),閉包等,看哪個(gè)占用大,并且內(nèi)容是什么
2.png
-
FPS 幀率
主要測(cè)試動(dòng)畫頁(yè)面刷新的頻率,頻率越高,動(dòng)畫越流暢
在 more tools > rendering > FPS meter 打開(kāi)
3.png
五、排查步驟
1.確認(rèn)問(wèn)題,找出可疑操作
先確認(rèn)是否真的存在內(nèi)存泄漏:
切換到Performance面板,開(kāi)始記錄(有必要從頭記的話)
開(kāi)始記錄 -> 操作 -> 停止記錄 -> 分析 -> 重復(fù)確認(rèn)
確認(rèn)存在內(nèi)存泄漏的話,縮小范圍,確定是什么交互操作引起的
也可以進(jìn)一步通過(guò)Memory面板的內(nèi)存分配時(shí)間軸來(lái)確認(rèn)問(wèn)題,Performance面板的優(yōu)勢(shì)是能看到DOM節(jié)點(diǎn)數(shù)和事件監(jiān)聽(tīng)器的變化趨勢(shì),甚至在沒(méi)有確定是內(nèi)存問(wèn)題拉低性能時(shí),還可以通過(guò)Performance面板看網(wǎng)絡(luò)響應(yīng)速度、CPU使用率等因素
2.分析堆快照,找出可疑對(duì)象
鎖定可疑的交互操作后,通過(guò)內(nèi)存快照進(jìn)一步深入:
切換到Memory面板,截快照1
做一次可疑的交互操作,截快照2
對(duì)比快照2和1,看數(shù)量Delta是否正常
再做一次可疑的交互操作,截快照3
對(duì)比3和2,看數(shù)量Delta是否正常,猜測(cè)Delta異常的對(duì)象數(shù)量變化趨勢(shì)
做10次可疑的交互操作,截快照4
對(duì)比4和3,驗(yàn)證猜測(cè),確定什么東西沒(méi)有被按預(yù)期回收
3.定位問(wèn)題,找到原因
鎖定可疑對(duì)象后,再進(jìn)一步定位問(wèn)題:
該類型對(duì)象的Distance是否正常,大多數(shù)實(shí)例都是3級(jí)4級(jí),個(gè)別到10級(jí)以上算異常
看路徑深度10級(jí)以上(或者明顯比其它同類型實(shí)例深)的實(shí)例,什么東西引用著它
4.釋放引用,修復(fù)驗(yàn)證
到這里基本找到問(wèn)題源頭了,接下來(lái)解決問(wèn)題:
想辦法斷開(kāi)這個(gè)引用
梳理邏輯流程,看其它地方是否存在不會(huì)再用的引用,都釋放掉
修改驗(yàn)證,沒(méi)解決的話重新定位
當(dāng)然,梳理邏輯流程在一開(kāi)始就可以做,邊用工具分析,邊確認(rèn)邏輯流程漏洞,雙管齊下,最后驗(yàn)證可以看Performance面板的趨勢(shì)折線或者M(jìn)emory面板的時(shí)間軸
六、記錄一次性能調(diào)試過(guò)程
瀏覽器執(zhí)行以下腳本
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);
- 初步看Memory內(nèi)存面板,發(fā)現(xiàn)Select JavaScript VM Instance的數(shù)值一值在變化,變化的規(guī)律是,20M左右的遞增,在100M到200M不定時(shí)的被垃圾回收,回到初始水平。最大可漲到400M。
- 接著看Performance性能面板,點(diǎn)擊開(kāi)始記錄,發(fā)現(xiàn)記錄10秒以上也不會(huì)停,此時(shí)手動(dòng)停止??吹絻?nèi)存線性遞增,沒(méi)有下降的地方。點(diǎn)擊JS Heap的高處,都是定位到Timer Fired,說(shuō)明是定時(shí)器有問(wèn)題,定時(shí)器下有個(gè)Function Call,說(shuō)明是他調(diào)用的函數(shù)有問(wèn)題。
- 接著我們又回到Memory面板,點(diǎn)擊開(kāi)始記錄,發(fā)現(xiàn)記錄會(huì)自動(dòng)停止,生成報(bào)告。此時(shí)內(nèi)存已經(jīng)漲到700M!了,連續(xù)點(diǎn)擊了幾記開(kāi)始記錄,生成了多份報(bào)告,發(fā)現(xiàn)每份報(bào)告的大小在500M左右。然后看Shadow Size 和 Retained Size 的數(shù)值,發(fā)現(xiàn) Shadow Size中 (string)的值占比98%,Retained Size的前5個(gè)占比98%。然后我展開(kāi)(string),發(fā)現(xiàn)大量1M左右的字符串,看來(lái)這個(gè)就是originalThing保存在內(nèi)存中,而且保存了非常多份。
- 最后,我們把unused閉包刪除,發(fā)現(xiàn)內(nèi)存占用大概下降了一半,再把originalThing刪除,就正常了。
參考原文鏈接:http://www.ayqy.net/blog/js%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E6%8E%92%E6%9F%A5%E6%96%B9%E6%B3%95/


