1、概述
Google Chrome
瀏覽器提供了很是強大的
JS
調試工具,
Heap Profiling
即是其中一個。
Heap Profiling
能夠記錄當前的堆內存(
heap
)快照,並生成對象的描述文件,該描述文件給出了當時
JS
運行所用到的全部對象,以及這些對象所佔用的內存大小、引用的層級關係等等。這些描述文件爲內存泄漏的排查提供了很是有用的信息。
注意:本文裏的全部例子均基於
Google Chrome
瀏覽器。
什麼是heap
JS
運行的時候,會有棧內存(
stack
)和堆內存(
heap
),當咱們用
new
實例化一個類的時候,這個
new
出來的對象就保存在
heap
裏面,而這個對象的引用則存儲在
stack
裏。程序經過
stack
裏的引用找到這個對象。例如
var a = [1,2,3];
,
a
是存儲在
stack
裏的引用,
heap
裏存儲着內容爲
[1,2,3]
的
Array
對象。
2、Heap Profiling
打開工具
打開
Chrome
瀏覽器(版本
25.0.1364.152 m),打開要監視的網站(這裏以遊戲大廳爲例),按下
F12
調出調試工具,點擊「
Profiles
」標籤。能夠看到下圖:
<ignore_js_op>
能夠看到,該面板能夠監控
CPU
、
CSS
和內存,選中「
Take Heap Snapshot
」,點擊「
Start
」按鈕,就能夠拍下當前
JS
的
heap
快照,以下圖所示:
<ignore_js_op>
右邊視圖列出了
heap
裏的對象列表。因爲遊戲大廳使用了
Quark
遊戲庫,因此這裏能夠清楚地看到
Quark.XXX
之類的類名稱(即
Function
對象的引用名稱)。
注意:每次拍快照前,都會先自動執行一次
GC
,因此在視圖裏的對象都是可及的。
視圖解釋
列字段解釋:
Constructor --
類名Distance --
估計是對象到根的引用層級距離
Objects Count --
給出了當前有多少個該類的對象
Shallow Size --
對象所佔內存(不包含內部引用的其它對象所佔的內存)
(
單位:字節
)
Retained Size --
對象所佔總內存(包含內部引用的其它對象所佔的內存)
(
單位:字節
)
下面解釋一下部分類名稱所表明的意思:
(compiled code) --
未知,估計是程序代碼區
(closure) --
閉包(array) --
未知
Object -- JS
對象類型(system) --
未知
(string) --
字符串類型,有時對象裏添加了新屬性,屬性的名稱也會出如今這裏
Array -- JS
數組類型cls --
遊戲大廳特有的繼承類
Window -- JS
的
window
對象
Quark.DisplayObjectContainer -- Quark
引擎的顯示容器類
Quark.ImageContainer -- Quark
引擎的圖片類
Quark.Text -- Quark
引擎的文本類
Quark.ToggleButton -- Quark
引擎的開關按鈕類
對於
cls
這個類名,是因爲遊戲大廳的繼承機制裏會使用「
cls
」這個引用名稱,指向新建的繼承類,因此凡是使用了該繼承機制的類實例化出來的對象,都放在這裏。例如程序中有一個類
ClassA
,繼承了
Quark.Text
,則
new
出來的對象是放在
cls
裏,不是放在
Quark.Text
裏。
查看對象內容
點擊類名左邊的三角形,能夠看到全部該類的對象。對象後面的「
@70035
」表示的是該對象的
ID
(有人會錯認爲是內存地址,
GC
執行後,內存地址是會變的,但對象
ID
不會)。把鼠標停留在某一個對象上,會顯示出該對象的內部屬性和當時的值。
<ignore_js_op>
這個視圖有助於咱們辨別這是哪一個對象。但該視圖跟蹤不了是被誰引用了。
查看對象的引用關係
點擊其中一個對象,能看到對象的引用層級關係,以下圖:
<ignore_js_op>
Object's retaining tree
視圖顯示出了該對象被哪些對象引用了,以及這個引用的名稱。圖中的這個對象被
5
個對象引用了,分別是:
1. 一個
cls
對象的
_txtContent
變量;
2. 一個閉包函數的
context
變量;
3. 同一個閉包函數的
self
變量;
4. 一個數組對象的
0
位置;
5. 一個
Quark.Tween
對象的
target
變量。
看到
context
和
self
這兩個引用,能夠知道這個
Quark.Text
對象使用了
JS
經常使用的上下文綁定機制,被一個閉包裏的變量引用着,至關於該
Quark.Text
對象多了兩個引用,這種狀況比較容易出現內存泄漏,若是閉包函數不釋放,這個
Quark.Text
對象也釋放不了。
展開
_textContent
,能夠看到下一級的引用:
<ignore_js_op>
把這個樹狀圖反過來看,能夠看到,該對象
(ID @70035)
其中的一條引用鏈是這樣的:
GameListV _curV _gameListV
省略
...
\ | /
\ | /
_noticeWidget
|
_noticeC
|
_noticeV
|
_txtContent
||
Quark.Text @70035
內存快照的對比經過快照對比的功能,能夠知道程序在運行期間哪些對象變動了。
剛纔已經拍下了一個快照,接下來再拍一次,以下圖:
<ignore_js_op>
點擊圖中的黑色實心圓圈按鈕,便可獲得第二個內存快照:
<ignore_js_op>
而後點擊圖中的「
Snapshot 2
」,視圖纔會切換到第二次拍的快照。
<ignore_js_op>
點擊圖中的「
Summary
」,可彈出一個列表,選擇「
Comparison
」選項,結果以下圖:
<ignore_js_op>
這個視圖列出了當前視圖與上一個視圖的對象差別。列名字段解釋:# New --
新建了多少個對象# Deleted --
回收了多少個對象# Delta --
對象變化值,即新建的對象個數減去回收了的對象個數Size Delta --
變化的內存大小
(
字節
)注意
Delta
字段,尤爲是值大於
0
的對象。下面以
Quark.Tween
爲例子,展開該對象,可看到以下圖所示:
<ignore_js_op>
在「
# New
」列裏,若是有「
.
」,則表示是新建的對象。
在「
# Deleted
」列裏,若是有「
.
」,則表示是回收了的對象。
平時排查問題的時候,應該多拍幾回快照進行對比,這樣有利於找出其中的規律。
3、內存泄漏的排查
JS
程序的內存溢出後,會使某一段函數體永遠失效(取決於當時的
JS
代碼運行到哪個函數),一般表現爲程序忽然卡死或程序出現異常。
這時咱們就要對該
JS
程序進行內存泄漏的排查,找出哪些對象所佔用的內存沒有釋放。這些對象一般都是開發者覺得釋放掉了
,但事實上仍被某個閉包引用着,或者放在某個數組裏面。
觀察者模式引發的內存泄漏
有時咱們須要在程序中加入觀察者模式(
Observer
)來解藕一些模塊,但若是使用不當,也會帶來內存泄漏的問題。
排查這類型的內存泄漏問題,主要重點關注被引用的對象類型是閉包(
closure
)和數組
Array
的對象。
下面以德州撲克遊戲爲例:
<ignore_js_op>
<ignore_js_op>
測試人員發現德州撲克遊戲存在內存溢出的問題,重現步驟:進入遊戲
--
退出到分區
--
再進入遊戲
--
再退出到分區,如此反覆幾回便出現遊戲卡死的問題。
排查的步驟以下:
1.打開遊戲;
2.進入第一個分區(快速場
5/10
);
3.進入後,拍下內存快照;
4.退出到剛纔的分區界面;
5.再次進入同一個分區;
6.進入後,再次拍下內存快照;
7.重複步驟
2
到
6
,直到拍下
5
組內存快照;
8.將每組的視圖都轉換到
Comparison
對比視圖;
9.進行內存對比分析。
通過上面的步驟後,能夠獲得下圖結果:
<ignore_js_op>
先看最後一個快照,能夠看到閉包(
closure
)
+1
,這是須要重點關注的部分。(
string
)、(
system
)和(
compiled code
)類型能夠無論,由於提供的信息很少。
<ignore_js_op>
接着點擊倒數第二個快照,看到閉包(
closure
)類型也是
+1
。
<ignore_js_op>
接着再看上一個快照,閉包仍是
+1
。
這說明每次進入遊戲都會建立這個閉包函數,而且退出到分區的時候沒有銷燬。
展開(
closure
),能夠看到很是多的
function
對象:
<ignore_js_op>
建新的閉包數量是
49
個,回收的閉包數量是
48
個,便是說此次操做有
48
個閉包正確釋放了,有一個忘記釋放了。每一個新建和回收的
function
對象的
ID
都不同,找不到任何的關聯性,沒法定位是哪個閉包函數出了問題。
接下來打開
Object's retaining tree
視圖,查找引用裏是否存在不斷增大的數組。
以下圖,展開「
Snapshot 5
」每一個
function
對象的引用:
<ignore_js_op>
其中有個
function
對象的引用
deleFunc
存放在一個數組裏,下標是
4
,數組的對象
ID
是
@45599
。
繼續查找「
Snapshot 4
」的
function
對象:
<ignore_js_op>
發現這裏有一個
function
的引用名稱也是
deleFunc
,也存放在
ID
爲
@45599
的數組裏,下標是
3
。這個對象極有多是沒有釋放掉的閉包。
繼續查看「
Snapshot 3
」裏的
function
對象:
<ignore_js_op>
從圖中能夠看到同一個
function
對象,下標是
2
。那麼這裏必定存在內存泄漏問題。
數組下面有一個引用名稱「
login_success
」,在程序裏搜索一下該關鍵字,終於定位到有問題的代碼。由於進入遊戲的時候註冊了「
login_success
」通知:
ob.addListener("login_success", _onLoginSuc);
但退出到分區的時候,沒有移除該通知,下次進入遊戲的時候,又再註冊了一次,因此形成
function
不斷增長。改爲退出到分區的時候移除該通知:
ob.removeListener("login_success", _onLoginSuc);
這樣就成功解決這個內存泄漏的問題了。
德州撲克這種問題多數見於觀察者設計模式中,使用一個全局數組存儲全部註冊的通知,若是忘記移除通知,則該數組會不斷增大,最終形成內存溢出。
上下文綁定引發的內存泄漏
不少時候咱們會用到上下文綁定函數
bind
(也有些人寫成
delegate
),不管是本身實現的
bind
方法仍是
JS
原生的
bind
方法,都會有內存泄漏的隱患。
下面舉一個簡單的例子: <script type="text/javascript"> var ClassA = function(name){ this.name = name; this.func = null; }; var a = new ClassA("a"); var b = new ClassA("b"); b.func = bind(function(){ console.log("I am " + this.name); }, a); b.func(); //輸出 I am a a = null; //釋放a //b = null; //釋放b //模擬上下文綁定 function bind(func, self){ return function(){ return func.apply(self); }; }; </script>
上面的代碼中, bind 經過閉包來保存上下文 self ,使得事件 b.func 裏的 this 指向的是 a ,而不是 b 。 首先咱們把 b = null; 註釋掉,只釋放 a 。看一下內存快照:
<ignore_js_op>
能夠看到有兩個
ClassA
對象,這與咱們的本意不相符,咱們釋放了
a
,應該只存在一個
ClassA
對象
b
纔對。
<ignore_js_op>
從上面兩個圖能夠看出這兩個對象中,一個是
b
,另外一個並非
a
,由於
a
這個引用已經置空了。第二個
ClassA
對象是
bind
裏的閉包的上下文
self
,
self
與
a
引用同一個對象。雖然
a
釋放了,但因爲
b
沒有釋放,或者
b.func
沒有釋放,使得閉包裏的
self
也一直存在。要釋放
self
,能夠執行
b=null
或者
b.func=null
。
把代碼改爲:
<script type="text/javascript"> var ClassA = function(name){ this.name = name; this.func = null; }; var a = new ClassA("a"); var b = new ClassA("b"); b.func = bind(function(){ console.log("I am " + this.name); }, a); b.func(); //輸出 I am a a = null; //釋放a b.func = null; //釋放self //模擬上下文綁定 function bind(func, self){ return function(){ return func.apply(self); }; }; </script>
再看看內存:
<ignore_js_op>
能夠看到只剩下一個
ClassA
對象
b
了,
a
已被釋放掉了。
4、結語
JS
的靈活性既是優勢也是缺點,平時寫代碼時要注意內存泄漏的問題。當代碼量很是龐大的時候,就不能僅靠複查代碼來排查問題,必需要有一些監控對比工具來協助排查。
以前排查內存泄漏問題的時候,總結出如下幾種常見的狀況:
1.閉包上下文綁定後沒有釋放;
2.
觀察者模式在添加通知後,沒有及時清理掉;
3.
定時器的處理函數沒有及時釋放,沒有調用
clearInterval
方法;
4.
視圖層有些控件重複添加,沒有移除。
|