JavaScript內(nèi)存泄露排查、垃圾回收理解、性能優(yōu)化調(diào)試

一、內(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ì)被垃圾回收
解決:

  1. 不要定義全局變量
  2. '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
  1. 如果沒(méi)有treeRef,leafRef的引用,移除tree,leaf節(jié)點(diǎn),就會(huì)直接移除,且不會(huì)保留在內(nèi)存
  2. 如果有treeRef,leafRef的引用,把tree的引用和節(jié)點(diǎn)移除,但leafRef的引用仍然會(huì)導(dǎo)致通過(guò)他就能找到(訪問(wèn))其它所有節(jié)點(diǎn),都給標(biāo)記上活躍,不會(huì)被清除
  3. 就是說(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)記清除算法步驟如下:

  1. GC維護(hù)一個(gè)root列表,root通常是代碼中持有引用的全局變量。JS中,window對(duì)象就是一例作為root的全局變量。window對(duì)象一直存在,所以GC認(rèn)為它及其所有孩子一直存在(非垃圾)

  2. 所有root都會(huì)被檢查并標(biāo)記為活躍(非垃圾),其所有孩子也被遞歸檢查。能通過(guò)root訪問(wèn)到的所有東西都不會(huì)被當(dāng)做垃圾

  3. 所有沒(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)試
  1. Performance 性能
    主要看內(nèi)存的變化情況,若一直上升,則有內(nèi)存泄露的問(wèn)題
    5.png
  1. Memory 內(nèi)存
    主要是看內(nèi)存中存儲(chǔ)什么東西
    比如數(shù)組,對(duì)象,函數(shù),閉包等,看哪個(gè)占用大,并且內(nèi)容是什么


    2.png
  1. 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);
  1. 初步看Memory內(nèi)存面板,發(fā)現(xiàn)Select JavaScript VM Instance的數(shù)值一值在變化,變化的規(guī)律是,20M左右的遞增,在100M到200M不定時(shí)的被垃圾回收,回到初始水平。最大可漲到400M。
  2. 接著看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)題。
  3. 接著我們又回到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)存中,而且保存了非常多份。
  4. 最后,我們把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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容