你需要思考的問題
整體來說,當你覺得你遇到了記憶體洩漏問題時,你需要思考三個問題:
我的頁面是否佔用了過多的記憶體? - Timeline記憶體檢視工具(Timeline memory view) 和 Chrome任務管理(Chrome task manager) 能幫助你確認你是否使用了過多的內存。 Memory view 能追蹤頁面渲染過程中DOM節點計數,documents文件計數和JS##事件監聽計數。作為一個經驗法則:避免對不再需要用到的DOM元素的引用,移除不需要的事件監聽並且在存儲你可能不會用到的大塊數據時要留意。
我的頁面有沒有記憶體洩漏? - 物件分配追蹤(Object allocation tracker)透過即時查看JS物件的分配來幫助你定位洩漏。你也可以使用堆分析儀(Heap Profiler)來產生JS堆快照,透過分析記憶體圖和比較快照之間的差異,來找出沒有被垃圾回收清理掉的物件。
我的頁面垃圾強制回收有多頻繁? - 如果你的頁面垃圾回收很頻繁,那表示你的頁面可能記憶體使用分配太頻繁了。 Timeline記憶體檢視工具(Timeline memory view) 能夠幫助你發現有興趣的停頓。
基本概念
#本小節介紹在記憶體分析時使用的常用術語,這些術語在為其它語言做記憶體分析的工具中也適用。這裡的術語和概念用在了堆分析儀(Heap Profiler)UI工具和相關的文件中。
這些能夠幫助我們熟悉如何有效的使用記憶體分析工具。如果你曾經用過像Java、.NET等語言的記憶體分析工具的話,那麼這將會是一個複習。
物件大小(Object sizes)把記憶體想像成一個包含基本型別(像數字和字串)和物件(關聯陣列 )的圖表。它可能看起來像是下面這幅一系列相關聯的點組成的圖。
一個物件有兩種使用記憶體的方法:- #物件本身直接使用
- 隱含的保持對其它物件的引用,這種方式會阻止垃圾回收(簡稱GC)對那些物件的自動回收處理。
直接佔用記憶體(Shallow Size)和佔用總記憶體(Retained Size),那它們是什麼意思呢?
直接佔用記憶體(Shallow Size,不包括引用的物件所佔用的記憶體)
這個是物件本身所佔用的記憶體。
典型的JavaScript物件都會有保留記憶體用來描述這個物件和儲存它的直接值。一般,只有陣列和字串會有明顯的直接佔用記憶體(Shallow Size)。但字串和陣列常常會在渲染器記憶體中儲存主要資料部分,僅在JavaScript物件堆疊中暴露一個很小的包裝物件。
渲染器記憶體是指你分析的頁面在渲染的過程中所用到的所有記憶體:頁面本身的記憶體+ 頁面中的JS堆疊到的記憶體+ 頁面觸發的相關工作行程(workers)中的JS堆用到的記憶體。然而,透過阻止垃圾自動回收別的對象,一個小對像都有可能間接佔用大量的記憶體。
佔用總記憶體(Retained Size,包括引用的物件所佔用的記憶體)
一個物件一但刪除後面它所引用的依賴物件就不能被GC根(GC root)引用到,它們所佔用的記憶體就會被釋放,一個物件佔用總記憶體包括這些依賴物件所佔用的記憶體。
GC根是由控制器(han#dles)組成的,這些控制器(不論是局部還是全域)是在建立由build-in函數(native code)到V8引擎以外的JavaScript物件的參考時創建的。所有這些控制器都能夠在堆疊快照的GC roots(GC根) > Handle scope 和 GC roots >Global handlers中找到。如果不深入了解瀏覽器的實作原理,在這篇文章中介紹這些控制器可能會讓人無法理解。 GC根和控制器你都不需要太在意。
有很多內部的GC根對使用者來說都是不重要的。從應用的角度來說有以下幾種情況:
Window 全域物件 (所有iframe中的)。在堆快照中有一個distance字段,它是從window物件到達對應物件的最短路徑長度。
由所有document能夠遍歷到的DOM節點組成的文檔DOM樹。不是所有節點都會被對應的JS引用,但有JS引用的節點在document存在的情況下都會被保留。
有很多物件可能是在調試程式碼時或DevTools console中(例如:console中的一些程式碼執行結束後)創建出來的。
注意:我們推薦使用者在建立堆疊快照時,不要在console中執行程式碼,也不要啟用偵錯斷點。
記憶體圖由一個根部開始,可能是瀏覽器的window
物件或Node.js模組Global
物件。這些物件如何被記憶體回收不受使用者的控制。
不能被GC根遍歷到的物件都會被記憶體回收。
注意:直接佔用記憶體和佔用總記憶體欄位中的資料是用位元組表示的。
物件的佔用總記憶體樹
之前我們已經了解到,堆是由各種互相關聯的物件組成的網狀結構。在數字領域,這種結構被稱為圖或記憶體圖。圖是由邊緣(edges)連接的節點(nodes)組成的,他們都被貼了標籤。
節點(Nodes) (或物件) 節點的標籤名稱是由建立他們的建構(constructor)函數的名稱確定
邊緣(Edges) 標籤名稱就是屬性名稱
支配物件(Dominators)
支配物件就像一個樹狀結構,因為每個物件都有一個支配者。一個對象的支配者可能不會直接引用它所支配的對象,就是說,支配對象樹結構不是圖中的生成樹。
在上圖:
#節點1支配節點2
節點2支配節點3,4和6
節點3支配節點5
節點5支配節點8
-
#節點6支配節點7
在下圖的例子中,節點#3
是#10
的支配者,但#7
也在每個從GC到#10
的路經中都出現了。像這樣,如果B物件在每個從根節點到A物件的路經中都出現,那麼B物件就是A物件的支配物件。
V8介紹
在本節,我們將描述一些記憶體相關的概念,這些概念是和V8 JavaScript虛擬機器 (V8 VM 或VM)有關的。當分析記憶體時,了解這些概念對理解堆快照是有幫助的。
JavaScript物件描述
有三個原始類型:
##布林值(Booleans) (true或false)字元型(Strings) (如'Werner Heisenberg')
#它們不會引用別的值,它們只會是葉子節點或終止節點。
- 數字(Numbers)
以下面兩種方式之一被儲存:
31位元
直接值,稱做:小整數(small integer
s)##堆對象,引用為堆值。堆值是用來儲存不適合用SMI形式儲存的數據,像雙精度數(doubles),或是當一個值需要被打包(boxed)
時,如給這個值再設定屬性值。字元型資料會以下列兩種方式儲存:
VM堆,或
外部的- 渲染器記憶體
- 中。這時會建立一個包裝物件用來存取儲存的位置,例如,Web頁麵包存的腳本資源和其它內容,而不是直接複製到VM堆中。
- 新建立的JavaScript物件會被在JavaScript堆疊上(或
VM堆疊
)分配記憶體。這些物件由V8的垃圾回收器管理,只要還有一個強引用他們就會在記憶體中保留。
本地物件
是所有不在JavaScript堆中的對象,與堆疊物件不同的是,在它們的生命週期中,不會被V8垃圾加收器處理,只能透過JavaScript包裝物件引用。 連接字串
是由一對字串合併成的對象,是合併後的結果。連接字串
只在有需要時合併。像一連接字串的子字串需要被建構時。 例如:如果你連接###a###和###b###,你得到字串(a, b)這用來表示連接的結果。如果你之後要再把這個結果與###d###連接,你就得到了另一個連接字串((a, b), d)。 #########陣列(###Array###s)### - 陣列是數字類型鍵的物件。它們在V8引擎中儲存大數據量的資料時被廣泛的使用。像字典這種有鍵-值對的物件就是用陣列實現的。 ######一個典型的JavaScript物件可以透過兩種陣列類型之一的方式來儲存:#############命名屬性,和########## ##數位化的元素############如果只有少量的屬性,它們會直接儲存在JavaScript物件本身中。 ############Map###### - 一種用來描述物件類型和它的結構的物件。例如,maps會被用來描述物件的結構以實現對物件屬性的快速存取######物件群組######每個本地物件群組都是由一組之間相互關聯的物件組成的。例如一個DOM子樹,每個節點都能存取它的父元素,下一個子元素和下一個兄弟元素,它們構成了關聯圖。需要注意的是本地元素沒有在JavaScript堆中表現——這就是它們的大小是零的原因,而它的包裝物件被創建了。 ###每個包裝物件都會有一個到本地物件的引用,用來傳遞對這些本地物件的操作。這些本地物件也有到包裝物件的參考。但這並不會創造無法收回的循環,GC是足夠聰明的,能夠分辨出那些已經沒有引用包裝物件的本地物件並釋放它們的。但如果有一個包裝物件沒有被釋放那它將會保留所有物件群組和相關的包裝物件。
先決條件與有用提示
Chrome 任務管理器
注意: 當使用Chrome做記憶體分析時,最好設定一個潔淨的測試環境
打開Chrome的記憶體管理器,觀察記憶體字段,在一個頁面上做相關的操作,你可以很快定位這個操作是否會導致頁面佔用很多記憶體。你可以從Chrome選單 > 工具或按Shift + Esc,找到記憶體管理器。
開啟後,在標頭右鍵選取 JavasScript使用的記憶體 這項。
透過DevTools Timeline來定位記憶體問題
解決問題的第一步就是要能夠證明問題存在。這就需要建立一個可重現的測試來做為問題的基準量測。沒有可重現的程序,就不能可靠的度量問題。換句話說如果沒有基準來做對比,就無法知道是哪些改變使問題出現的。
時間軸面版(Timeline panel)對於發現程式何時出了問題很用幫助。它展示了你的web應用程式或網站加載和互動的時刻。所有的事件:從載入資源到解JavaScript,樣式計算,垃圾回收停頓和頁面重繪。都在時間軸上表示出來了。
當分析記憶體問題時,時間軸面版上的記憶體檢視(Memory view)能用來觀察:
-
使用的總記憶體– 記憶體使用成長了什麼?
DOM節點數
- ##文件(documents)數
- 註冊的事件監聽器(event listeners)數
證明一個問題的存在
首先要做的事情是找出你認為可能導致內存洩漏的一些動作。可以是發生在頁面上的任何事件,滑鼠移入,點擊,或其它可能會導致頁面效能下降的互動。 在時間軸面版上開始記錄(Ctrl+E 或 Cmd+E)然後做你想要測試的動作。想要強制進行垃圾回收點面版上的垃圾筒圖示()。
下面是一個記憶體洩漏的例子,有些點沒有被垃圾回收: #如果經過一些反覆測試後,你看到的是鋸齒狀的圖形(在記憶體面版的上方),說明你的程式中有很多短時存在的物件。而如果一系列的動作沒有讓記憶體保持在一定的範圍,而DOM節點數沒有回到開始時的數目,你就可以懷疑有記憶體洩漏了。 一旦確定了記憶體上存在的問題,你就可以使用分析面板(Profiles panel)上的堆分析儀(heap profiler)來定位問題的來源。
範例: 試試memory growth的例子,能幫助你有效的記憶體回收記憶體回收器(像V8中的)需要能夠定位哪些物件是活的(live),而那些被認為是死的(垃圾)的物件是無法引用到的(unreachable)。
如果垃圾回收 (GC)因為JavaScript執行時有邏輯錯誤而沒有能夠回收到垃圾對象,這些垃圾對象就無法再被重新回收了。像這樣的情況最終會讓你的應用越來越慢。
例如你在寫程式碼時,有的變數和事件監聽器已經用不到了,但是卻還是被有些程式碼引用。只要引用還存在,那被引用的物件就無法被GC正確的回收。
當你的應用程式在運行中,有些DOM物件可能已經更新/移除了,要記住檢查引用了DOM物件的變數並將其設定null。檢查可能會引用到其它物件(或其它DOM元素)的物件屬性。雙眼要盯著可能越來越成長的變數快取。
堆分析儀
拍一個快照
在Profiles面板中,選擇Take Heap Snapshot,然後點選Start或按Cmd + E或Ctrl + E:
快照最初是儲存在渲染器進程記憶體中的。它們被按需匯入到了DevTools中,當你點擊快照按鈕後就可以看到它們了。當快照被載入DevTools中顯示後,快照標題下面的數字顯示了能夠被引用到的(reachable)JavaScript物件佔有記憶體總數。
範例:試試看garbage collection in action的例子,在時間軸(Timeline)面板中監控記憶體的使用。
清除快照
注意:關閉DevTools視窗並不能從渲染記憶體中刪除掉收集的快照。當重新開啟DevTools後,先前的快照清單還在。
記住我們之前提到的,當你產生快照時你可以強制執行在DevTools中GC。當我們拍快照時,GC是自動執行的。在時間軸(Timeline)中點選垃圾桶(垃圾回收)按鈕()就可以輕鬆的執行垃圾回收了。
範例:試試看scattered objects並用堆疊分析儀(Heap Profiler)分析它。你可以看到(對象)項目的集合。
切換快照檢視
一個快照可以根據不同的任務切換檢視。可以透過如圖的選擇框切換:
以下是三個預設視圖:
##Summary(概要) - 透過建構函數名分類顯示物件;
#Comparison(對照) - 顯示兩個快照間物件的差異;
Containment(控制) - 可用來探測堆內容;
Dominators(支配者)視圖可以在Settings面板中開啟– 顯示dominators tree. 可以用來找到記憶體增長點。
透過不同顏色區分物件物件的屬性和屬性值有不同的類型並自動的透過顏麼進行了區分。每個屬性都是以下四種之一:a:property - 透過名稱索引的普通屬性,由.(點)運算子,或[](中括號)引用,如["foo bar"];
0:element - 透過數字索引的普通屬性,由[](中括號)引用;
a:context var - 函數內的屬性,在函數上下文內,透過名稱引用;
a:system prop - 由JavaScript VM 新增的屬性,JavaScript程式碼不能存取。
System的物件沒有對應的JavaScript類型。它們是JavaScript VM物件系統內建的。 V8將大多數內建物件和使用者JS物件放在同一個堆中。但它們只是V8的
內部物件。
displaying object totals, which can be expanded to show instances:
第一層級是」總體」行,它們顯示了:
Constructor(建構子)表示所有透過此建構函式產生的物件
物件的實例數在Objects Count列上顯示
#Shallow size列顯示了由對應建構子產生的物件的shallow sizes(直接佔用記憶體)總數
Retained size列展示了對應物件所佔用的最大記憶體
Distance列顯示的是物件到達GC根的最短距離
展開一個總體行後,會顯示所有的物件實例。沒一個實例的直接佔用記憶體和占用總記憶體都被對應顯示。 @符號後的數字不物件的唯一ID,有了它你就可以逐個物件的在不同快照間作對比。
範例:試試這個範例(在新tab標籤中開啟)來了解如何使用概要檢視。
記住黃色的物件被JavaScript引用,而紅色的物件是由黃色背景色引用被分離了的節點。
Comparison view(對照視圖)
此視圖用來對照不同的快照來找出快照之間的差異,來發現有記憶體洩漏的物件。來證明對應用的某個操作沒有造成洩漏(例如:一般一對操作和撤消的動作,像找開一個document,然後關閉,這樣是不會造成洩漏的),你可以按以下的步驟嘗試:
在操作前拍一個堆疊快照;
執行一個動作(做你認為會造成洩漏的動作);
執行一個動作(做你認為會造成洩漏的動作);
#撤銷先前的操作(上一個操作相反的操作,多重複幾次);
#拍第二個快照,將視圖切換成對照視圖,並同快照1進行比較。
在對照視圖下,兩個快照之間的差異就會展現出來了。當展開一個總類別目後,增加和刪除了的物件就顯示出來了:#範例:嘗試範例(在新tab標籤中開啟)來了解如何使用對照視圖來定位記憶體洩漏。
Containment view(控制視圖)
- 控制視圖可以稱作對你的應用的物件結構的」鳥瞰視圖(bird’s eys view)」。它能讓你查看function內部,像你的JavaScript對像一樣的觀察VM內部對象,能讓你在你的應用的非常低層的記憶體使用情況。
此檢視提供了幾個進入點:
- DOMWindow 物件
- 這些物件是JavaScript程式碼的」全域」物件;
- GC根
- VM的垃圾回收器真正的GC根;
##Native物件
下圖是一個典型的控制視圖:
範例:試試範例(在新tab標籤中開啟)來了解如何使用控制視圖來檢視閉包
內部和事件處理。
關於閉包的建議