js記憶體泄露學習(轉)

來源:互聯網
上載者:User

標籤:blog   http   java   使用   os   檔案   io   for   

http://blog.csdn.net/kaitiren/article/details/19974269記憶體泄露不錯的文章,感謝分享

Google Chrome瀏覽器提供了非常強大的JS調試工具,Heap Profiling便是其中一個。Heap Profiling可以記錄當前的堆記憶體(heap)快照,並產生對象的描述檔案,該描述檔案給出了當時JS運行所用到的所有對象,以及這些對 ...  
一、概述
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對象。
二、Heap Profiling
開啟工具
開啟Chrome瀏覽器(版本25.0.1364.152 m),開啟要監視的網站(這裡以遊戲大廳為例),按下F12調出調試工具,點擊“Profiles”標籤。可以看到:

可以看到,該面板可以監控CPU、CSS和記憶體,選中“Take Heap Snapshot”,點擊“Start”按鈕,就可以拍下當前JS的heap快照,如所示:

右邊視圖列出了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不會)。把滑鼠停留在某一個對象上,會顯示出該對象的內部屬性和當時的值。

這個視圖有助於我們辨別這是哪個對象。但該視圖跟蹤不了是被誰引用了。

查看對象的參考關聯性
點擊其中一個對象,能看到對象的引用層級關係,如:

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,可以看到下一級的引用:

把這個樹狀圖反過來看,可以看到,該對象(ID @70035)其中的一條引用鏈是這樣的:
GameListV       _curV       _gameListV    省略...                  \         |        /                    \       |       /                  _noticeWidget                           |                     _noticeC                           |                     _noticeV                           |                  _txtContent                           ||             Quark.Text @70035
記憶體快照的對比通過快照對比的功能,可以知道程式在運行期間哪些對象變更了。
剛才已經拍下了一個快照,接下來再拍一次,如:

點擊圖中的黑色實心圓圈按鈕,即可得到第二個記憶體快照:

然後點擊圖中的“Snapshot 2”,視圖才會切換到第二次拍的快照。

點擊圖中的“Summary”,可彈出一個列表,選擇“Comparison”選項,結果如:

這個視圖列出了當前視圖與上一個視圖的對象差異。列名欄位解釋:# New -- 建立了多少個對象# Deleted -- 回收了多少個對象# Delta -- 對象變化值,即建立的對象個數減去回收了的對象個數Size Delta -- 變化的記憶體大小(位元組)注意Delta欄位,尤其是值大於0的對象。下面以Quark.Tween為例子,展開該對象,可看到如所示:

在“# New”列裡,如果有“.”,則表示是建立的對象。
在“# Deleted”列裡,如果有“.”,則表示是回收了的對象。
平時排查問題的時候,應該多拍幾次快照進行對比,這樣有利於找出其中的規律。

三、記憶體流失的排查
JS程式的記憶體溢出後,會使某一段函數體永遠失效(取決於當時的JS代碼運行到哪一個函數),通常表現為程式突然卡死或程式出現異常。
這時我們就要對該JS程式進行記憶體流失的排查,找出哪些對象所佔用的記憶體沒有釋放。這些對象通常都是開發人員以為釋放掉了,但事實上仍被某個閉包引用著,或者放在某個數組裡面。

觀察者模式引起的記憶體流失
有時我們需要在程式中加入觀察者模式(Observer)來解藕一些模組,但如果使用不當,也會帶來記憶體流失的問題。
排查這類型的記憶體流失問題,主要重點關注被引用的物件類型是閉包(closure)和數組Array的對象。
下面以德州撲克遊戲為例:


測試人員發現德州撲克遊戲存在記憶體溢出的問題,重現步驟:進入遊戲--退出到分區--再進入遊戲--再退出到分區,如此反覆幾次便出現遊戲卡死的問題。
排查的步驟如下:
1.開啟遊戲;
2.進入第一個分區(快速場5/10);
3.進入後,拍下記憶體快照;
4.退出到剛才的分區介面;
5.再次進入同一個分區;
6.進入後,再次拍下記憶體快照;
7.重複步驟2到6,直到拍下5組記憶體快照;
8.將每組的視圖都轉換到Comparison對比視圖;
9.進行記憶體對比分析。
經過上面的步驟後,可以得到結果:

先看最後一個快照,可以看到閉包(closure)+1,這是需要重點關注的部分。(string)、(system)和(compiled code)類型可以不管,因為提供的資訊不多。

接著點擊倒數第二個快照,看到閉包(closure)類型也是+1。

接著再看上一個快照,閉包還是+1。
這說明每次進入遊戲都會建立這個閉包函數,並且退出到分區的時候沒有銷毀。
展開(closure),可以看到非常多的function對象:

建新的閉包數量是49個,回收的閉包數量是48個,即是說這次操作有48個閉包正確釋放了,有一個忘記釋放了。每個建立和回收的function對象的ID都不一樣,找不到任何的關聯性,無法定位是哪一個閉包函數出了問題。
接下來開啟Object‘s retaining tree視圖,尋找引用裡是否存在不斷增大的數組。
如,展開“Snapshot 5”每個function對象的引用:

其中有個function對象的引用deleFunc存放在一個數組裡,下標是4,數組的對象ID是@45599。
繼續尋找“Snapshot 4”的function對象:

發現這裡有一個function的引用名稱也是deleFunc,也存放在ID為@45599的數組裡,下標是3。這個對象極有可能是沒有釋放掉的閉包。
繼續查看“Snapshot 3”裡的function對象:

可以看到同一個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。看一下記憶體快照:


可以看到有兩個ClassA對象,這與我們的本意不相符,我們釋放了a,應該只存在一個ClassA對象b才對。

從上面兩個圖可以看出這兩個對象中,一個是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>
再看看記憶體:

可以看到只剩下一個ClassA對象b了,a已被釋放掉了。

四、結語
JS的靈活性既是優點也是缺點,平時寫代碼時要注意記憶體流失的問題。當代碼量非常龐大的時候,就不能僅靠複查代碼來排查問題,必須要有一些監控對比工具來協助排查。
之前排查記憶體流失問題的時候,總結出以下幾種常見的情況:
1.閉包上下文綁定後沒有釋放;
2.觀察者模式在添加通知後,沒有及時清理掉;
3.定時器的處理函數沒有及時釋放,沒有調用clearInterval方法;
4.視圖層有些控制項重複添加,沒有移除。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.