內(nèi)存泄漏是指計算機可用內(nèi)存的逐漸減少躬络。當(dāng)程序持續(xù)無法釋放其使用的臨時內(nèi)存時就會發(fā)生。JavaScript的web應(yīng)用也會經(jīng)常遇到在原生應(yīng)用程序中出現(xiàn)的內(nèi)存相關(guān)的問題然磷,如泄漏和溢出匪燕,web應(yīng)用也需要應(yīng)對垃圾回收停頓。
<article style="display: block;">盡管JavaScript使用垃圾回收進(jìn)行自動內(nèi)存管理榕订,但有效的(effective)內(nèi)存管理依然很重要。在這篇文章中我們將探討分析JavaScript web應(yīng)用中的內(nèi)存問題蜕便。在學(xué)習(xí)有關(guān)特性時請確保嘗試一下相關(guān)案例以提高你對這些工具在實踐中如何工作的認(rèn)識劫恒。請閱讀內(nèi)存 101(Memory 101)頁面來幫助你熟悉這篇文章中用到的術(shù)語。注意:我們將要用到的某些特性目前僅對Chrome Canary版瀏覽器可用轿腺。我們推薦使用這個版本來獲得最佳的工具两嘴,以分析你的應(yīng)用程序的內(nèi)存問題。
你需要思考的問題
總體來說族壳,當(dāng)你覺得你遇到了內(nèi)存泄漏問題時憔辫,你需要思考三個問題:
- 我的頁面是否占用了過多的內(nèi)存? - Timeline內(nèi)存查看工具(Timeline memory view) 和 Chrome任務(wù)管理(Chrome task manager) 能幫助你確認(rèn)你是否使用了過多的內(nèi)存。Memory view 能跟蹤頁面渲染過程中DOM節(jié)點計數(shù)仿荆,documents文檔計數(shù)和JS事件監(jiān)聽計數(shù)贰您。作為一個經(jīng)驗法則:避免對不再需要用到的DOM元素的引用坏平,移除不需要的事件監(jiān)聽并且在存儲你可能不會用到的大塊數(shù)據(jù)時要留意。
- 我的頁面有沒有內(nèi)存泄漏? - 對象分配跟蹤(Object allocation tracker)通過實時查看JS對象的分配來幫助你定位泄漏锦亦。你也可以使用堆分析儀(Heap Profiler)生成JS堆快照舶替,通過分析內(nèi)存圖和比較快照之間的差異,來找出沒有被垃圾回收清理掉的對象杠园。
- 我的頁面垃圾強制回收有多頻繁? - 如果你的頁面垃圾回收很頻繁顾瞪,那說明你的頁面可能內(nèi)存使用分配太頻繁了。Timeline內(nèi)存查看工具(Timeline memory view) 能夠幫助你發(fā)現(xiàn)感興趣的停頓抛蚁。
術(shù)語和基本概念
本小節(jié)介紹在內(nèi)存分析時使用的常用術(shù)語陈醒,這些術(shù)語在為其它語言做內(nèi)存分析的工具中也適用。這里的術(shù)語和概念用在了堆分析儀(Heap Profiler)UI工具和相關(guān)的文檔中瞧甩。
這些能夠幫助我們熟悉如何有效的使用內(nèi)存分析工具钉跷。如果你曾用過像Java、.NET等語言的內(nèi)存分析工具的話肚逸,那么這將是一個復(fù)習(xí)爷辙。
對象大小(Object sizes)
把內(nèi)存想象成一個包含基本類型(像數(shù)字和字符串)和對象(關(guān)聯(lián)數(shù)組)的圖表。它可能看起來像下面這幅一系列相關(guān)聯(lián)的點組成的圖吼虎。
一個對象有兩種使用內(nèi)存的方法:
- 對象自身直接使用
- 隱含的保持對其它對象的引用犬钢,這種方式會阻止垃圾回收(簡稱GC)對那些對象的自動回收處理。
當(dāng)你使用DevTools中的堆分析儀(Heap Profiler思灰,用來分析內(nèi)存問題的工具玷犹,在DevTools的”Profile”標(biāo)簽下)時,你可能會驚喜的發(fā)現(xiàn)一些顯示各種信息的欄目洒疚。其中有兩項是:直接占用內(nèi)存(Shallow Size)和占用總內(nèi)存(Retained Size)歹颓,那它們是什么意思呢?
直接占用內(nèi)存(Shallow Size油湖,不包括引用的對象占用的內(nèi)存)
這個是對象本身占用的內(nèi)存巍扛。
典型的JavaScript對象都會有保留內(nèi)存用來描述這個對象和存儲它的直接值。一般乏德,只有數(shù)組和字符串會有明顯的直接占用內(nèi)存(Shallow Size)撤奸。但字符串和數(shù)組常常會在渲染器內(nèi)存中存儲主要數(shù)據(jù)部分,僅僅在JavaScript對象棧中暴露一個很小的包裝對象喊括。
渲染器內(nèi)存指你分析的頁面在渲染的過程中所用到的所有內(nèi)存:頁面本身的內(nèi)存 + 頁面中的JS堆用到的內(nèi)存 + 頁面觸發(fā)的相關(guān)工作進(jìn)程(workers)中的JS堆用到的內(nèi)存胧瓜。然而,通過阻止垃圾自動回收別的對象郑什,一個小對象都有可能間接占用大量的內(nèi)存府喳。
占用總內(nèi)存(Retained Size,包括引用的對象所占用的內(nèi)存)
一個對象一但刪除后它引用的依賴對象就不能被GC根(GC root)引用到蘑拯,它們所占用的內(nèi)存就會被釋放钝满,一個對象占用總內(nèi)存包括這些依賴對象所占用的內(nèi)存兜粘。
GC根是由控制器(handles)組成的,這些控制器(不論是局部還是全局)是在建立由build-in函數(shù)(native code)到V8引擎之外的JavaScript對象的引用時創(chuàng)建的弯蚜。所有這些控制器都能夠在堆快照的GC roots(GC根) > Handle scope 和 GC roots >Global handlers中找到孔轴。如果不深入了解瀏覽器的實現(xiàn)原理,在這篇文章中介紹這些控制器可能會讓人不能理解熟吏。GC根和控制器你都不需要過多關(guān)心距糖。
有很多內(nèi)部的GC根對用戶來說都是不重要的玄窝。從應(yīng)用的角度來說有下面幾種情況:
- Window 全局對象 (所有iframe中的)牵寺。在堆快照中有一個distance字段,它是從window對象到達(dá)對應(yīng)對象的最短路徑長度恩脂。
- 由所有document能夠遍歷到的DOM節(jié)點組成的文檔DOM樹帽氓。不是所有節(jié)點都會被對應(yīng)的JS引用挡育,但有JS引用的節(jié)點在document存在的情況下都會被保留肤寝。
- 有很多對象可能是在調(diào)試代碼時或者DevTools console中(比如:console中的一些代碼執(zhí)行結(jié)束后)創(chuàng)建出來的裆操。
注意:我們推薦用戶在創(chuàng)建堆快照時韭畸,不要在console中執(zhí)行代碼臂港,也不要啟用調(diào)試斷點仲智。
內(nèi)存圖由一個根部開始娘汞,可能是瀏覽器的window
對象或Node.js模塊Global
對象烈钞。這些對象如何被內(nèi)存回收不受用戶的控制漫仆。
不能被GC根遍歷到的對象都將被內(nèi)存回收捎拯。
注意:直接占用內(nèi)存和占用總內(nèi)存字段中的數(shù)據(jù)是用字節(jié)表示的。
對象的占用總內(nèi)存樹
之前我們已經(jīng)了解到盲厌,堆是由各種互相關(guān)聯(lián)的對象組成的網(wǎng)狀結(jié)構(gòu)署照。在數(shù)字領(lǐng)域,這種結(jié)構(gòu)被稱為圖或內(nèi)存圖吗浩。圖是由邊緣(edges)連接著的節(jié)點(nodes)組成的建芙,他們都被貼了標(biāo)簽。
- 節(jié)點(Nodes) (或?qū)ο?/em>) 節(jié)點的標(biāo)簽名是由創(chuàng)建他們的構(gòu)造(constructor)函數(shù)的名稱確定
- 邊緣(Edges) 標(biāo)簽名就是屬性名
本文檔的后面你將了解到如何使用堆分析儀生成快照懂扼。從下圖的堆分析儀生成的快照中禁荸,我們能看到距離(distance)這個字段:是指對象到GC根的距離。如果同一個類型的所有對象的距離都一樣阀湿,而有一小部分的距離卻比較大赶熟,那么就可能出了些你需要進(jìn)行調(diào)查的問題了。
支配對象(Dominators)
支配對象就像一個樹結(jié)構(gòu)炕倘,因為每個對象都有一個支配者钧大。一個對象的支配者可能不會直接引用它支配的對象,就是說罩旋,支配對象樹結(jié)構(gòu)不是圖中的生成樹啊央。
在上圖中:
- 節(jié)點1支配節(jié)點2
- 節(jié)點2支配節(jié)點3眶诈,4和6
- 節(jié)點3支配節(jié)點5
- 節(jié)點5支配節(jié)點8
- 節(jié)點6支配節(jié)點7
在下圖的例子中,節(jié)點#3
是#10
的支配者瓜饥,但#7
也在每個從GC到#10
的路經(jīng)中都出現(xiàn)了逝撬。像這樣,如果B對象在每個從根節(jié)點到A對象的路經(jīng)中都出現(xiàn)乓土,那么B對象就是A對象的支配對象宪潮。
V8介紹
在本節(jié),我們將描述一些內(nèi)存相關(guān)的概念趣苏,這些概念是和V8 JavaScript虛擬機(V8 VM 或VM)有關(guān)的狡相。當(dāng)分析內(nèi)存時,了解這些概念對理解堆快照是有幫助的食磕。
JavaScript對象描述
有三個原始類型:
- 數(shù)字(Numbers) (如 3.14159..)
- 布爾值(Booleans) (true或false)
- 字符型(Strings) (如 ‘Werner Heisenberg’)
它們不會引用別的值尽棕,它們只會是葉子節(jié)點或終止節(jié)點。
數(shù)字(Numbers)以下面兩種方式之一被存儲:
- 31位整數(shù)直接值彬伦,稱做:小整數(shù)(small integers)(SMIs)滔悉,或
- 堆對象,引用為堆值单绑。堆值是用來存儲不適合用SMI形式存儲的數(shù)據(jù)回官,像雙精度數(shù)(doubles),或者當(dāng)一個值需要被打包(boxed)時搂橙,如給這個值再設(shè)置屬性值歉提。
字符型數(shù)據(jù)會以下面兩種方式存儲:
- VM堆,或
- 外部的渲染器內(nèi)存中份氧。這時會創(chuàng)建一個包裝對象用來訪問存儲的位置唯袄,比如,Web頁面包存的腳本資源和其它內(nèi)容蜗帜,而不是直接復(fù)制至VM堆中恋拷。
新創(chuàng)建的JavaScript對象會被在JavaScript堆上(或VM堆)分配內(nèi)存。這些對象由V8的垃圾回收器管理厅缺,只要還有一個強引用他們就會在內(nèi)存中保留蔬顾。
本地對象是所有不在JavaScript堆中的對象,與堆對象不同的是湘捎,在它們的生命周期中诀豁,不會被V8垃圾加收器處理,只能通過JavaScript包裝對象引用窥妇。
連接字符串是由一對字符串合并成的對象舷胜,是合并后的結(jié)果。連接字符串只在有需要時合并活翩。像一連接字符串的子字符串需要被構(gòu)建時烹骨。
比如:如果你連接a和b翻伺,你得到字符串(a, b)這用來表示連接的結(jié)果。如果你之后要再把這個結(jié)果與d連接沮焕,你就得到了另一個連接字符串((a, b), d)吨岭。
數(shù)組(Arrays) - 數(shù)組是數(shù)字類型鍵的對象。它們在V8引擎中存儲大數(shù)據(jù)量的數(shù)據(jù)時被廣泛的使用峦树。像字典這種有鍵-值對的對象就是用數(shù)組實現(xiàn)的辣辫。
一個典型的JavaScript對象可以通過兩種數(shù)組類型之一的方式來存儲:
- 命名屬性,和
- 數(shù)字化的元素
如果只有少量的屬性魁巩,它們會被直接存儲在JavaScript對象本身中急灭。
Map - 一種用來描述對象類型和它的結(jié)構(gòu)的對象。比如歪赢,maps會被用來描述對象的結(jié)構(gòu)以實現(xiàn)對對象屬性的快速訪問
對象組
每個本地對象組都是由一組之間相互關(guān)聯(lián)的對象組成的化戳。比如一個DOM子樹单料,每個節(jié)點都能訪問到它的父元素埋凯,下一個子元素和下一個兄弟元素,它們構(gòu)成了一個關(guān)聯(lián)圖扫尖。需要注意的是本地元素沒有在JavaScript堆中表現(xiàn)-這就是它們的大小是零的原因白对,而它的包裝對象被創(chuàng)建了。
每個包裝對象都會有一個到本地對象的引用换怖,用來傳遞對這些本地對象的操作甩恼。這些本地對象也有到包裝對象的引用。但這并不會創(chuàng)造無法收回的循環(huán)沉颂,GC是足夠智能的条摸,能夠分辨出那些已經(jīng)沒有引用包裝對象的本地對象并釋放它們的。但如果有一個包裝對象沒有被釋放那它將會保留所有對象組和相關(guān)的包裝對象铸屉。
先決條件和有用提示
Chrome 任務(wù)管理器
注意: 當(dāng)使用Chrome做內(nèi)存分析時钉蒲,最好設(shè)置一個潔凈的測試環(huán)境
打開Chrome的內(nèi)存管理器,觀察內(nèi)存字段彻坛,在一個頁面上做相關(guān)的操作顷啼,你可以很快定位這個操作是否會導(dǎo)致頁面占用很多內(nèi)存。你可以從Chrome菜單 > 工具或按Shift + Esc昌屉,找到內(nèi)存管理器钙蒙。
打開后,在標(biāo)頭右擊選用 JavasScript使用的內(nèi)存 這項间驮。
通過DevTools Timeline來定位內(nèi)存問題
解決問題的第一步就是要能夠證明問題存在躬厌。這就需要創(chuàng)建一個可重現(xiàn)的測試來做為問題的基準(zhǔn)度量。沒有可再現(xiàn)的程序竞帽,就不能可靠的度量問題扛施。換句話說如果沒有基準(zhǔn)來做為對比偏陪,就無法知道是哪些改變使問題出現(xiàn)的。
時間軸面版(Timeline panel)對于發(fā)現(xiàn)程序什么時候出了問題很用幫助煮嫌。它展示了你的web應(yīng)用或網(wǎng)站加載和交互的時刻笛谦。所有的事件:從加載資源到解JavaScript,樣式計算昌阿,垃圾回收停頓和頁面重繪饥脑。都在時間軸上表示出來了。
當(dāng)分析內(nèi)存問題時懦冰,時間軸面版上的內(nèi)存視圖(Memory view)能用來觀察:
- 使用的總內(nèi)存 – 內(nèi)存使用增長了么?
- DOM節(jié)點數(shù)
- 文檔(documents)數(shù)
- 注冊的事件監(jiān)聽器(event listeners)數(shù)
更多的關(guān)于在內(nèi)存分析時灶轰,定位內(nèi)存泄漏的方法,請閱Zack Grossbart的Memory profiling with the Chrome DevTools
證明一個問題的存在
首先要做的事情是找出你認(rèn)為可能導(dǎo)致內(nèi)存泄漏的一些動作刷钢∷癫可以是發(fā)生在頁面上的任何事件,鼠標(biāo)移入内地,點擊伴澄,或其它可能會導(dǎo)致頁面性能下降的交互。
在時間軸面版上開始記錄(Ctrl+E 或 Cmd+E)然后做你想要測試的動作阱缓。想要強制進(jìn)行垃圾回收點面版上的垃圾筒圖標(biāo)([圖片上傳失敗...(image-459677-1569383413904)] )非凌。
下面是一個內(nèi)存泄漏的例子,有些點沒有被垃圾回收:
如果經(jīng)過一些反復(fù)測試后荆针,你看到的是鋸齒狀的圖形(在內(nèi)存面版的上方)敞嗡,說明你的程序中有很多短時存在的對象。而如果一系列的動作沒有讓內(nèi)存保持在一定的范圍航背,并且DOM節(jié)點數(shù)沒有返回到開始時的數(shù)目喉悴,你就可以懷疑有內(nèi)存泄漏了。
一旦確定了存在內(nèi)存上的問題玖媚,你就可以使用分析面板(Profiles panel)上的堆分析儀(heap profiler)來定位問題的來源箕肃。
例子: 嘗試一下memory growth的例子,能幫助你有效的練習(xí)通過時間軸分析內(nèi)存問題最盅。
內(nèi)存回收
內(nèi)存回收器(像V8中的)需要能夠定位哪些對象是活的(live)突雪,而那些被認(rèn)為是死的(垃圾)的對象是無法引用到的(unreachable)。
如果垃圾回收 (GC)因為JavaScript執(zhí)行時有邏輯錯誤而沒有能夠回收到垃圾對象涡贱,這些垃圾對象就無法再被重新回收了咏删。像這樣的情況最終會讓你的應(yīng)用越來越慢。
比如你在寫代碼時问词,有的變量和事件監(jiān)聽器已經(jīng)用不到了督函,但是卻仍然被有些代碼引用。只要引用還存在,那被引用的對象就無法被GC正確的回收辰狡。
當(dāng)你的應(yīng)用程序在運行中锋叨,有些DOM對象可能已經(jīng)更新/移除了,要記住檢查引用了DOM對象的變量并將其設(shè)null宛篇。檢查可能會引用到其它對象(或其它DOM元素)的對象屬性娃磺。雙眼要盯著可能會越來越增長的變量緩存。
堆分析儀
拍一個快照
在Profiles面板中叫倍,選擇Take Heap Snapshot偷卧,然后點擊Start或者按Cmd + E或者Ctrl + E:
快照最初是保存在渲染器進(jìn)程內(nèi)存中的。它們被按需導(dǎo)入到了DevTools中吆倦,當(dāng)你點擊快照按鈕后就可以看到它們了听诸。當(dāng)快照被載入DevTools中顯示后,快照標(biāo)題下面的數(shù)字顯示了能夠被引用到的(reachable)JavaScript對象占有內(nèi)存總數(shù)蚕泽。
例子:嘗試一下garbage collection in action的例子晌梨,在時間軸(Timeline)面板中監(jiān)控內(nèi)存的使用。
清除快照
點擊Clear all按鈕圖標(biāo)([圖片上傳失敗...(image-dbe8df-1569383413904)] )须妻,就能清除掉所有快照:
注意:關(guān)閉DevTools窗口并不能從渲染內(nèi)存中刪除掉收集的快照仔蝌。當(dāng)重新打開DevTools后,之前的快照列表還在璧南。
記住我們之前提到的掌逛,當(dāng)你生成快照時你可以強制執(zhí)行在DevTools中GC。當(dāng)我們拍快照時司倚,GC是自動執(zhí)行的。在時間軸(Timeline)中點擊垃圾桶(垃圾回收)按鈕([圖片上傳失敗...(image-2ed01-1569383413904)] )就可以輕松的執(zhí)行垃圾回收了篓像。
例子:嘗試一下scattered objects并用堆分析儀(Heap Profiler)分析它动知。你可以看到(對象)項目的集合。
切換快照視圖
一個快照可以根據(jù)不同的任務(wù)切換視圖员辩『辛福可以通過如圖的選擇框切換:
下面是三個默認(rèn)視圖:
- Summary(概要) - 通過構(gòu)造函數(shù)名分類顯示對象;
- Comparison(對照) - 顯示兩個快照間對象的差異奠滑;
- Containment(控制) - 可用來探測堆內(nèi)容丹皱;
Dominators(支配者)視圖可以在Settings面板中開啟 – 顯示dominators tree. 可以用來找到內(nèi)存增長點。
通過不同顏色區(qū)分對象
對象的屬性和屬性值有不同的類型并自動的通過顏么進(jìn)行了區(qū)分宋税。每個屬性都是以下四種之一:
- a:property - 通過名稱索引的普通屬性摊崭,由.(點)操作符,或引用杰赛,如["foo bar"]呢簸;
- 0:element - 通過數(shù)字索引的普通屬性,由引用;
- a:context var - 函數(shù)內(nèi)的屬性根时,在函數(shù)上下文內(nèi)瘦赫,通過名稱引用;
- a:system prop - 由JavaScript VM 添加的屬性蛤迎,JavaScript代碼不能訪問确虱。
命名為System
的對象沒有對應(yīng)的JavaScript類型。它們是JavaScript VM對象系統(tǒng)內(nèi)置的替裆。V8將大多數(shù)內(nèi)置對象和用戶JS對象放在同一個堆中蝉娜。但它們只是V8的內(nèi)部對象。
視圖詳解
Summary view(概要視圖)
打開一個快照扎唾,默認(rèn)是以概要視圖顯示的召川,顯示了對象總數(shù),可以展開顯示具體內(nèi)容: Initially, a snapshot opens in the Summary view, displaying object totals, which can be expanded to show instances:
第一層級是”總體”行胸遇,它們顯示了:
- Constructor(構(gòu)造函數(shù))表示所有通過該構(gòu)造函數(shù)生成的對象
- 對象的實例數(shù)在Objects Count列上顯示
- Shallow size列顯示了由對應(yīng)構(gòu)造函數(shù)生成的對象的shallow sizes(直接占用內(nèi)存)總數(shù)
- Retained size列展示了對應(yīng)對象所占用的最大內(nèi)存
- Distance列顯示的是對象到達(dá)GC根的最短距離
展開一個總體行后荧呐,會顯示所有的對象實例。沒一個實例的直接占用內(nèi)存和占用總內(nèi)存都被相應(yīng)顯示纸镊。@符號后的數(shù)字不對象的唯一ID倍阐,有了它你就可以逐個對象的在不同快照間作對比。
例子:嘗試這個例子(在新tab標(biāo)簽中打開)來了解如何使用概要視圖逗威。
記住黃色的對象被JavaScript引用峰搪,而紅色的對象是由黃色背景色引用被分離了的節(jié)點。
Comparison view(對照視圖)
該視圖用來對照不同的快照來找到快照之間的差異凯旭,來發(fā)現(xiàn)有內(nèi)存泄漏的對象概耻。來證明對應(yīng)用的某個操作沒有造成泄漏(比如:一般一對操作和撤消的動作,像找開一個document罐呼,然后關(guān)閉鞠柄,這樣是不會造成泄漏的),你可以按以下的步驟嘗試:
- 在操作前拍一個堆快照嫉柴;
- 執(zhí)行一個操作(做你認(rèn)為會造成泄漏的動作)厌杜;
- 撤消之前的操作(上一個操作相反的操作,多重復(fù)幾次)计螺;
- 拍第二個快照夯尽,將視圖切換成對照視圖,并同快照1進(jìn)行對比登馒。
在對照視圖下匙握,兩個快照之間的不同就會展現(xiàn)出來了。當(dāng)展開一個總類目后谊娇,增加和刪除了的對象就顯示出來了:
例子:嘗試例子(在新tab標(biāo)簽中打開)來了解如何使用對照視圖來定位內(nèi)存泄漏肺孤。
Containment view(控制視圖)
控制視圖可以稱作對你的應(yīng)用的對象結(jié)構(gòu)的”鳥瞰視圖(bird’s eys view)”罗晕。它能讓你查看function內(nèi)部,跟你的JavaScript對象一樣的觀察VM內(nèi)部對象赠堵,能讓你在你的應(yīng)用的非常低層的內(nèi)存使用情況小渊。
該視圖提供了幾個進(jìn)入點:
- DOMWindow 對象 - 這些對象是JavaScript代碼的”全局”對象;
- GC根 - VM的垃圾回收器真正的GC根茫叭;
- Native對象 - 瀏覽器對象對”推入”JavaScript虛擬機中來進(jìn)行自動操作酬屉,如:DOM節(jié)點,CSS規(guī)則(下一節(jié)會有詳細(xì)介紹揍愁。)
下圖是一個典型的控制視圖:
例子:嘗試例子(在新tab標(biāo)簽中打開)來了解如何使用控制視圖來查看閉包內(nèi)部和事件處理呐萨。
關(guān)于閉包的建議
給函數(shù)命名對你在快照中的閉包函數(shù)間作出區(qū)分會很用幫助。如:下面的例子中沒有給函數(shù)命名:
</article>
<pre class="brush: actionscript3; gutter: true; first-line: 1 hljs php" style="margin: 15px auto; padding: 10px 15px; display: block; overflow-x: auto; color: rgb(51, 51, 51); background: rgb(251, 251, 251); word-break: break-all; overflow-wrap: break-word; white-space: pre-wrap; font: 12px/20px "courier new"; border-width: 1px 1px 1px 4px; border-style: solid; border-color: rgb(221, 221, 221); border-image: initial;">function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function() { // this is NOT a named function
return largeStr;
};
return lC;
}</pre>
<article style="display: block;">而下面這個有給函數(shù)命名:</article>
<pre class="brush: actionscript3; gutter: true; first-line: 1 hljs php" style="margin: 15px auto; padding: 10px 15px; display: block; overflow-x: auto; color: rgb(51, 51, 51); background: rgb(251, 251, 251); word-break: break-all; overflow-wrap: break-word; white-space: pre-wrap; font: 12px/20px "courier new"; border-width: 1px 1px 1px 4px; border-style: solid; border-color: rgb(221, 221, 221); border-image: initial;">function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function lC() { // this IS a named function
return largeStr;
};
return lC;
}</pre>
例子:嘗試這個例子why eval is evil來分析內(nèi)存中閉包的影響莽囤。你可能也對嘗試下面這個例子谬擦,記錄heap allocations(堆分配)有興趣。
揭露DOM內(nèi)存泄漏
這個工具獨一無二的一點是展示了瀏覽器原生對象(DOM節(jié)點朽缎,CSS規(guī)則)和JavaScript對象之間的雙向引用惨远。這能幫助你發(fā)現(xiàn)因為忘記解除引用游離的DOM子節(jié)點而導(dǎo)致的難以發(fā)覺的內(nèi)存泄漏。
DOM內(nèi)存泄漏可能會超出你的想象话肖”被啵看下下面的例子 – #tree對象什么時候被GC呢?
<pre class="brush: actionscript3; gutter: true; first-line: 1 hljs cs" style="margin: 15px auto; padding: 10px 15px; display: block; overflow-x: auto; color: rgb(51, 51, 51); background: rgb(251, 251, 251); word-break: break-all; overflow-wrap: break-word; white-space: pre-wrap; font: 12px/20px "courier new"; border-width: 1px 1px 1px 4px; border-style: solid; border-color: rgb(221, 221, 221); border-image: initial;">var select = document.querySelector;
var treeRef = select("#tree");
var leafRef = select("#leaf");
var body = select("body");
body.removeChild(treeRef);
//#tree can't be GC yet due to treeRef
treeRef = null;
//#tree can't be GC yet due to indirect
//reference from leafRef
leafRef = null;
//#NOW can be #tree GC</pre>
#leaf
代表了對它的父節(jié)點的引用(parentNode)它遞歸引用到了#tree
最筒,所以贺氓,只有當(dāng)leafRef被nullified后#tree
代表的整個樹結(jié)構(gòu)才會被GC回收。
例子:嘗試leaking DOM nodes來了解哪里DOM節(jié)點會內(nèi)存泄漏并如何定位床蜘。你也可以看一下這個例子:DOM leaks being bigger than expected辙培。
查看Gonzalo Ruiz de Villa的文章Finding and debugging memory leaks with the Chrome DevTools來閱讀更多關(guān)于DOM內(nèi)存泄漏和內(nèi)存分析的基礎(chǔ)。
原生對象在Summary和Containment視呼中更容易找到 – 有它們專門的類目:
例子:嘗試下這個例子(在新tab標(biāo)簽中打開)來了解如何將DOM樹分離悄泥。
支配者視圖(Dominators view)
支配者視圖顯示了堆圖的支配者樹虏冻。支配者視圖跟控制(Containment)視圖很像,但是沒有屬性名弹囚。這是因為支配者可能會是一個沒有直接引用的對象,就是說這個支配者樹不是堆圖的生成樹领曼。但這是個有用的視圖能幫助我們很快的定位內(nèi)存增長點鸥鹉。
注意:在Chrome Canary中,支配者視圖能夠在DevTools中的Settings > Show advanced heap snapshot properties 開啟庶骄,重啟DevTools生效毁渗。
例子:嘗試這個例子(在新tab標(biāo)簽中打開)來練習(xí)如何找到內(nèi)存增長點〉サ螅可以進(jìn)一步嘗試下一個例子retaining paths and dominators灸异。
對象分配跟蹤器
對象跟蹤器整合了heap profiler的快照增量更新分析和Timeline面板的記錄。跟其它工具一樣,記錄對象的堆配置需要啟動記錄肺樟,執(zhí)行一系列操作檐春,然后停止記錄然后進(jìn)行分析。
對象跟蹤器不間斷的記錄堆快照(頻率達(dá)到了每50毫秒么伯!)疟暖,結(jié)束時記錄最后一個快照。該堆分配分析器顯示對象在哪被創(chuàng)建并定位它的保留路徑田柔。
開啟并使用對象分析器
開始使用對象分析器: 1. 確認(rèn)你使用的是最新版的Chrome Canary俐巴。
- 打開DeveTools并點擊齒輪圖標(biāo)(譯者:沒明白這步有什么用)。
- 現(xiàn)在硬爆,打開Profiler面板欣舵,你就能看到”Record Heap Allocations”的選項。
上面的柱條表示在堆中生成的新對象缀磕。高度就對應(yīng)了相應(yīng)對象的大小缘圈,它的顏色表示了這個對象是否在最后拍的那個快照中還在:藍(lán)色柱表示在timeline最后這個對象還在,灰色柱表示這個對象在timeline中生成虐骑,但結(jié)束前已經(jīng)被內(nèi)存回收了准验。
上面的例子中,一個動作執(zhí)行了10次廷没。同一個程序保留了5個對象糊饱,所以最后5個藍(lán)色柱條被保留了。但這最后留下的柱存在潛在的問題颠黎。你可以用timeline上的滑動條縮小到那個特定的快照并找到這個分配的對象另锋。
點擊一個堆中的對象就能在堆快照的下面部分顯示它的保留總內(nèi)存樹。檢查這個對象的保留總內(nèi)存樹能夠給你足夠的信息來了解為什么這個對象沒有被回收狭归,然后你就能對代碼做相應(yīng)的修改來去掉不必要的引用夭坪。
內(nèi)存分析FAQ
問:我不能看到對象的所有屬性,我也看到它們的非字符串值过椎!為什么室梅?
并非所有屬性都完整的保存在JavaScript堆中。其中有些是通過執(zhí)行原生代碼的getters方法來獲取的疚宇。這些屬性沒有在堆快照中捕獲亡鼠,是為了防止對getters方法的調(diào)用和避免程序狀態(tài)的改變,如果這些getters方法不是”純(Pure)”的functions敷待。同樣间涵,非字符串的值,如數(shù)字榜揖,沒有被捕獲是為了減少快照的大小勾哩。
問:@符號后面的數(shù)字是什么意思 – 是地址還是ID呢抗蠢?這個ID值真的是唯一的么?
這是對象ID思劳。顯示對象的地址沒有意義迅矛,因為一個對象會在垃圾回收的時候被移除。這些對象IDs是真正的IDs – 就是說敢艰,它們在不同的快照間是唯一表示的诬乞。這樣就可以的堆狀態(tài)間進(jìn)行精確的對比。維持這些IDs會給GC流程增加額外的開支钠导,但這僅在記錄第一次堆快照時分配 – 如果堆分析儀沒有用到震嫉,就不會有額外的開支。
問:”死”(無法引用到的)對象被包含在快照中了么牡属?
沒有票堵,只有可以引用到的對象才會顯示在快照中。而且逮栅,拍快照前都會先自動執(zhí)行GC操作悴势。
注意:在寫這篇文章的時候,我們計劃在拍快照的時候不再GC措伐,防止堆尺寸的減少√叵耍現(xiàn)在已經(jīng)是這樣了,但垃圾對象依然顯示在快照之外侥加。
問:GC根是由什么組成的捧存?
由很多部分組成:
- 原生對象圖;
- 符號表担败;
- VM線程中的棧昔穴;
- 編輯緩存;
- 控制器上下文提前;
- 全局控制器吗货。
問:我得知可以使用Heap Profiler和Timeline Memory view來檢測內(nèi)存泄漏。但我應(yīng)該先用哪個工具呢狈网?
Timeline面版宙搬,是在你第一次使用你的頁面發(fā)現(xiàn)速度變慢了時用來論斷過多的內(nèi)存使用。網(wǎng)站變慢是比較典型的內(nèi)存泄漏的信號拓哺,但也可能是其它的原因 – 可能是有渲染或網(wǎng)絡(luò)傳輸方面的瓶頸害淤,所以要確保解決你網(wǎng)頁的真正問題。
論斷是否是內(nèi)存問題拓售,就打開Timeline面板和Memory標(biāo)簽。點擊record按鈕镶奉,然后在你的應(yīng)用上重復(fù)幾次你認(rèn)為可能導(dǎo)致內(nèi)存泄漏的操作础淤。停止記錄崭放。你應(yīng)用的內(nèi)存使用圖就生成出來了。如果內(nèi)存的使用一直在增長(而沒有相應(yīng)的下降)鸽凶,這就表明你的應(yīng)用可能有內(nèi)存泄漏了币砂。
一般一個正常的應(yīng)用的內(nèi)存使用圖形是鋸齒狀的,因為內(nèi)存使用后又會被垃圾回收器回收玻侥。不用擔(dān)心這種鋸齒形 – 因為總是會因為JavaScript而有內(nèi)存的消耗决摧,甚至一個空的requestAnimationFrame
也會造成這種鋸齒形,這是無法避免的凑兰。只要不是那種分配了持續(xù)很多內(nèi)存的形狀掌桩,那就表明生成了很多內(nèi)存垃圾。
上圖的增長線是需要你警惕的姑食。在診斷分析的時候Memory標(biāo)簽中的DOM node counter波岛,Document counter和Event listener count也是很有用的。DOM節(jié)點數(shù)是使用的原生內(nèi)存不會影響JavaScript內(nèi)存圖音半。
一旦你確認(rèn)你的應(yīng)用有內(nèi)存泄漏则拷,堆分析儀就可以用來找到內(nèi)存泄漏的地方。
問:我發(fā)現(xiàn)堆快照中有的DOM節(jié)點的數(shù)字是用紅色標(biāo)記為”Detached DOM tree”曹鸠,而其它的是黃色的煌茬,這是什么意思呢?
你會發(fā)現(xiàn)有不同的顏色彻桃。紅色的節(jié)點(有著深色的背景)沒有從JavaScript到它們的直接的引用坛善,但它們是分離出來的DOM結(jié)構(gòu)的一部分,所以他們還是在內(nèi)存中保留了叛薯。有可能有一個節(jié)點被JavaScript引用到了(可能是在閉包中或者一個變量)浑吟,這個引用會阻止整個DOM樹被內(nèi)存回收。
黃色節(jié)點(黃色背景)有JavaScript的直接引用耗溜。在同一個分離的DOM樹中查看一個黃色的節(jié)點來定位你的JavaScript的引用组力。就可能看到從DOM window到那個節(jié)點的屬性引用鏈(如:window.foo.bar[2].baz
)。
下面的動態(tài)圖顯示了分離節(jié)點的處理過程:
例子:嘗試這個例子detached nodes你可以查看節(jié)點在Timeline中的生命周期抖拴,然后拍堆快照來找到分離的節(jié)點燎字。
問:直接占用內(nèi)存(Shallow Size)和占用總內(nèi)存(Retained Size)分別代表什么,它們的區(qū)別是什么阿宅?
是這樣的候衍,對象可以在內(nèi)存中以兩種方式存在(be alive) – 直接的被別一個可訪問的(alive)對象保留(window和document對象總是可訪問的)或被原生對象(象DOM對象)隱含的包留引用。后一種方式會因為阻止對象被GC自動回收洒放,而有導(dǎo)制內(nèi)存泄泥漏的可能蛉鹿。對象自身占用的內(nèi)存被稱為直接占用內(nèi)存(通常來說,數(shù)組和字符串會保留更多的直接占用內(nèi)存(shallow size))往湿。
一個任意大小的對象可以通過阻止其它對象內(nèi)存被回收在保留很大的內(nèi)存使用妖异。當(dāng)一個對象被刪除后(它造成的一些依賴就無法被引用了)能夠釋放的內(nèi)存的大小被稱有占用總內(nèi)存(retained size)惋戏。
問:constructor和retained字段下有很多的數(shù)據(jù)。我應(yīng)該從哪開始調(diào)查我是的否遇到了內(nèi)存泄漏呢他膳?
一般來說最好是從通過retainers排序的第一個對象開始响逢,retainers之間是通過距離排序的(是指到window對象的距離)包吝。
距離最短的對象有可能是首選的可能導(dǎo)致內(nèi)存泄漏的對象聘惦。
問:Summary, Comparison, Dominators 和 Containment這些視圖之間的不同是什么?
你可以通過切換視圖來體驗它們的區(qū)別谢肾。
- Summary(概要)視圖能幫你通過構(gòu)造函數(shù)分組尋找對象(和對象的內(nèi)存使用)蟀俊。該視圖對找出DOM內(nèi)存泄漏很有幫助钦铺。
- Comparison(對照)視圖能夠通過顯示哪些對象內(nèi)存被正確的回收了來搜尋內(nèi)存泄漏。通常在一個操作前后記錄兩個(或更多)的內(nèi)存使用快照欧漱。它是通過察看釋放的內(nèi)存和引用數(shù)目的差導(dǎo)來察看是否有內(nèi)存泄漏职抡,并找到原因。
- Containment(控制)視圖對對象結(jié)構(gòu)有更好的展示误甚,幫助我們分析全局作用域(如 window)中對象引用情況來找到是什么保留了這些對象缚甩。它能讓你分析閉包并深入到對象更深層去查看。
- Dominators(支配者)視圖能用來幫助我們確認(rèn)沒有多余的對象還掛在某個位置(如那些被引用了的)窑邦,和確認(rèn)對象的刪除/垃圾回收真正起了作用擅威。
問:堆分析儀中的constructor(一組)內(nèi)容代表什么?
- (global property) - 全局對象(像 ‘window’)和引用它的對象之間的中間對象冈钦。如果一個對象由構(gòu)造函數(shù)Person生成并被全局對象引用郊丛,那么引用路徑就是這樣的:[global] > (global property) > Person。這跟一般的直接引用彼此的對象不一樣瞧筛。我們用中間對象是有性能方面的原因厉熟,全局對象改變會很頻繁,非全局變量的屬性訪問優(yōu)化對全局變量來說并不適用较幌。
- (roots) - constructor中roots的內(nèi)容引用它所選中的對象揍瑟。它們也可以是由引擎自主創(chuàng)建的一些引用。這個引擎有用于引用對象的緩存乍炉,但是這些引用不會阻止引用對象被回收绢片,所以它們不是真正的強引用(FIXME)。
- ****(closure)** - 一些函數(shù)閉包中的一組對象的引用**
- (array, string, number, regexp) - 一組屬性引用了Array,String,Number或正則表達(dá)式的對象類型
- (compiled code) - 簡單來說岛琼,所有東西都與compoled code有關(guān)底循。Script像一個函數(shù),但其實對應(yīng)了<script>的內(nèi)容槐瑞。SharedFunctionInfos (SFI)是函數(shù)和compiled code之間的對象熙涤。函數(shù)通常有內(nèi)容,而SFIS沒有(FIXME)。
- ****HTMLDivElement, HTMLAnchorElement, DocumentFragment 等 – 你代碼中對elements或document對象的引用灭袁。
在你的程序的生命周期中生成的很多其它的對象猬错,包括事件監(jiān)聽器或自定義對象,可以在下面的controllers中找到:
問:我在做內(nèi)存分析時需要關(guān)閉Chrome里可能會產(chǎn)生影響的什么功能么茸歧?
我們建議在用Chrome DevTools做內(nèi)存分析時,你可以使用關(guān)閉所有擴展功能的隱身模式显沈,或設(shè)置用戶文件夾為(--user-data-dir=""
)后再打開Chrome软瞎。
應(yīng)用,擴展甚至console中的記錄都會對你的分析有潛在的影響拉讯,如果你想讓你的分析可靠的話涤浇,禁用這些吧。
寫在最后的話
今天的JavaScript引擎已經(jīng)具有很強的能力魔慷,能夠自動回收代碼產(chǎn)生的內(nèi)存垃圾只锭。就是說,它們只能做到這樣了院尔,但我們的應(yīng)用仍然被證明會因為邏輯錯誤而產(chǎn)生內(nèi)存泄漏蜻展。使用相應(yīng)的工具來找到應(yīng)用的瓶頸,記住邀摆,不要靠猜 – 測試它纵顾。
轉(zhuǎn)自http://www.codeceo.com/article/chrome-javascript-memory.html