JavaScript 內存泄露

          今天下午同事讓幫忙看web內存泄露問題。當時定位到創建ActiveX 對象的時候產生的,于是我對這個奇怪的問題進行了一些深入探索。 

          很多時候我都依賴javascript的垃圾回收機制,所以對以及C++ 操作內存語言常發生的內存泄露是很陌生的。當時創建回調函數用了閉包,當然最終的解決方法是也避免閉包調用。 

              

              隨著這個問題的浮出水面,我回憶起以前的一個項目中也應該存在這個內存泄露問題。于是查閱了相關資料把類似的問題總結下來,希望對大家也有幫助。

              原因:對于一門具有垃圾收回機制的語言存在內存泄露,其原因不外乎就是javascript腳本引擎存在bug

             很多時候,我們要做的不是去修正那樣的bug,而是想辦法去規避。

          目前發現的可能導致內存泄露的代碼有三種:

          · 循環引用

          · 自動類型裝箱轉換

          · 某些DOM操作

          下面具體的來說說內存是如何泄露的

          循環引用:這種方式存在于IE6FF2中(FF3未做測試),當出現了一個含有DOM對象的循環引用時,就會發生內存泄露。

          什么是循環引用?首先搞清楚什么是引用,一個對象A的屬性被賦值為另一個對象B時,則可以稱A引用了B。假如B也引用了A,那么AB之間構成了循環引用。同樣道理 如果能找到A引用B B引用C C又引用A這樣一組飲用關系,那么這三個對象構成了循環引用。當一個對象引用自己時,它自己形成了循環引用。注意,在js中變量永遠是對象的屬性,它可以指向對象,但決不是對象本身。

          循環引用很常見,而且通常是無害的,但如果循環引用中包含DOM對象或者ActiveX對象,那么就會發生內存泄露。例子:

          var a=document.createElement("div");
          var b=new Object();
          a.b=b;
          b.a=a; 

          很多情況下循環引用不是這樣的明顯,下面就是著名的閉包(closure)造成內存泄露的例子,每執行一次函數A()都會產生內存泄露。試試看,根據前面講的scope對象的知識,能不能找出循環引用?

          function A()...{
              var a=document.createElement("div");
              a.onclick=function()...{
                  alert("hi");
              }
          }
          A(); 

          OK, 讓我們來看看。假設A()執行時創建的作用域對象叫做ScopeA 找到以下引用關系
          ScopeA引用DOM對象document.createElement("div");
          DOM對象document.createElement("div");引用函數function(){alert("hi")}
          函數function(){alert("hi")}引用ScopeA

          這樣就很清楚了,所謂closure泄露,只不過是幾個js特殊對象的循環引用而已。

          自動類型裝箱轉換:這種泄露存在于ie6 ie7中。這是極其匪夷所思的一個bug,看下面代碼

          var s="lalalalala";
          alert(s.length); 

          這段代碼怎么了?看看吧,"lalalalala"已經泄露了。關鍵問題出在s.length上,我們知道js的類型中,string并非對象,但可以對它使用.運算符,為什么呢?因為js的默認類型轉換機制,允許js在遇到.運算符時自動將string轉換為object型中對應的String對象。而這個轉換成的臨時對象100%會泄露(汗一下)。

          某些DOM操作也可能導致泄露 這些惡心的bug只存在于ie系列中。在ie7中 因為試圖fix循環引用bug而讓情況變得更糟,以至于我對寫這一段種滿了恐懼。

          ie6談起,下面是微軟的例子,

          <html>
              <head>
                  <script language="JScript">...
                  function LeakMemory()
                  ...{
                      var hostElement = document.getElementById("hostElement");
                      // Do it a lot, look at Task Manager for memory response
                      for(i = 0; i < 5000; i++)
                      ...{
                          var parentDiv =
                              document.createElement("<div onClick='foo()'>");
                          var childDiv =
                              document.createElement("<div onClick='foo()'>");
                          // This will leak a temporary object
                          parentDiv.appendChild(childDiv);
                          hostElement.appendChild(parentDiv);
                          hostElement.removeChild(parentDiv);
                          parentDiv.removeChild(childDiv);
                          parentDiv = null;
                          childDiv = null;
                      }
                      hostElement = null;
                  }

                  function CleanMemory()
                  ...{
                      var hostElement = document.getElementById("hostElement");
                      // Do it a lot, look at Task Manager for memory response
                      for(i = 0; i < 5000; i++)
                      ...{
                          var parentDiv =
                              document.createElement("<div onClick='foo()'>");
                          var childDiv =
                              document.createElement("<div onClick='foo()'>");
                          // Changing the order is important, this won't leak
                          hostElement.appendChild(parentDiv);
                          parentDiv.appendChild(childDiv);
                          hostElement.removeChild(parentDiv);
                          parentDiv.removeChild(childDiv);
                          parentDiv = null;
                          childDiv = null;
                      }
                      hostElement = null;
                  }
                  </script>
              </head>
              <body>
                  <button onclick="LeakMemory()">Memory Leaking Insert</button>
                  <button onclick="CleanMemory()">Clean Insert</button>
                  <div id="hostElement"></div>
              </body>
          </html>

          看看結果吧,LeakMemory造成了內存泄露,而CleanMemory沒有,循環引用了么?仔細看看沒有。那么是什么問題呢?MS的解釋是"插入順序不對",必須先將父級元素appendChild。這聽起來有些模糊,這里給出一個比較恰當的等價描述:永遠不要使用DOM節點樹之外元素的appendChild方法

          我曾經看到過這樣的說法,創建dom的時候,先創建子節點,當子節點完善后一次性添加到頁面中,不要一點點朝頁面上加東西,盡量減少document刷新次數,這樣效率會高點。(打個比方就是應該像 LeakMemory )可見這里我還是被某些書籍誤導了。至少他沒有告訴我內存泄露的問題。

          接下來是ie7ie8 beta 1中運行這段程序,看到什么?沒看錯吧,2個都泄露了!別急,刷新一下頁面就好了。為什么呢?ie7改變了DOM元素的回收方式:在離開頁面時回收DOM樹上的所有元素,所以ie7下的內存管理非常簡單:在所有的頁面中只要掛在DOM樹上的元素,就不會泄露,沒掛在DOM樹上,肯定泄露。所以,ie7中記住一條原則:在離開頁面之前把所有創建的DOM元素掛到DOM樹上。

          接下來談談ie7的這個設計吧,坦白的說,這種做法純粹是偷懶的垃圾做法。動態垃圾回收不是保證所有內存都在離開頁面時收回,而是要保證內存的充分利用,運行時不回收,等到離開時回收有什么用?這只是名義上的避免泄露,其實是完全的泄露。況且還沒有回收DOM節點樹之外的元素。

           4.內存泄露的解決方案

          內存泄露怎么辦?真的以后不用閉包了么?沒法封裝控件了?這樣做還不如要了js程序員的命,嘿嘿。

          事實上,通過一些很簡單的小技巧,可以巧妙的繞開這些危險的bug

          to be continued......

          coming soon:

          · 顯式類型轉換

          · 避免事件導致的循環引用

          · 不影響返回值地打破循環引用

          · 延遲appendChild

          · 代理DOM對象

          · 顯式類型轉換

          首先說說最容易處理的情況 對于類型轉換造成的錯誤,我們可以通過顯式類型轉換來避免:

          var s=newString("lalalalala");//此處將string轉換成object
          alert(s.length); 

           這個太容易了,算不上正經方案。不過類型轉換泄露也就這一種處理方法了。

          · 避免事件導致的循環引用

          在比較成熟的js程序員里,把事件函數寫成閉包是再正常不過了:

          function A(){
              var a=document.createElement("div");
              a.onclick=function(){
                  alert("hi");
              }

          這將導致內存泄露。按照IBM那兩位老大的說法,當然是把函數放外面或者a=null就沒問題了,不過還要訪問A()里面的變量呢?假如有下面的代碼:

          function A(){
              var a=document.createElement("div");
              var b=document.createElement("div");
              a.onclick=function(){
                  alert(b.outerHTML);
              }
              return a;

           如何將它的邏輯表達出來 還避免內存泄露? 分析一下這個內存泄露的形式:只要onclick的外部環境中不包含a那么,就不會泄露。那么辦法有2個一是將環境到a的引用斷開 另一個是將function到環境的引用斷開,但是,如果要在函數中訪問b就不能將Function放到外面,如果要返回a的值,就不能a=null,怎么辦呢?

          解決方案1

          構造一個不含a的新環境

          function A(){
              var a=document.createElement("div");
              var b=document.createElement("div");
              a.onclick=BuildEvent(b);
              return a;
          }

          function BuildEvent(b)
          {
              return function(){
                  alert(b.outerHTML);
              }

          a本身可以通過this訪問,將其它需要訪問的外層函數變量傳遞給BuildEvent就可以了。保持BuildEvent定義和調用的參數名一致,會帶來方便。

          解決方案2

          return 之后a=null,不可能? 看看下面:

          function A(){
              try{
                  var a=document.createElement("div");
                  var b=document.createElement("div");
                  a.onclick= function(){
                      alert(b.outerHTML);
                  }
                  return a;
              } finally {
                  a=null;
              }

          finallytry之后執行,如果finall塊不返回值,才會返回try塊的返回值。

          · 延遲appendChild

          還記得函數的lazy initalize吧,對于ie惡心至極的DOM操作泄露,我們需要用類似的方法去處理。在一個函數中構造一個復雜對象,在需要的時候將之appendChildDOM樹上,這是很常見的做法,但在IE6中,這樣做將導致所謂的"插入順序內存泄露",沒有別的辦法,我們只能用一個數組parts保存子節點,編寫一個appendTo方法先序遍歷節點樹,去把它掛在某個DOM節點上。

          function appendTo(Element)
          ...{
              Element.appendChild(this);
              if(!this.parts)return;
              for(var i=0;i<this.parts.length;i++)
                  parts.appendTo(this);

           

          · 垃圾箱

          對于ie7,我比較無可奈何,因為DOM對象不會被CG程序回收,只有離開頁面時會被回收,所以我的建議是:使用DOM要有節制,盡量多用innerHTML...... good luck.

          一旦你使用了DOM對象,千萬不要試圖o=null,你可以設置一個叫做Garbagediv并且將其display設置為none,將不用的DOM對象存入其中(就是appendChild上去)就好了

          · 代理對象

          這是Ext的做法,這里只是順帶提一下。將每個元素用一個"代理對象"操作,不論appendChild還是其他操作都不是對DOM對象本身的操作,而是通過這個代理對象操作。這是一個很不錯的Proxy模式,不過要想避免泄露還是需要一點功夫的,并非用了Proxy之后就不會泄露,有時反而更容易泄露。

          5 .FAQ

          內存泄露是內存占用很大么? 不是,即使1byte內存也叫做內存泄露。

          程序中提示,內存不足,是內存泄露么?不是,這一般是無限遞歸函數調用導致棧內存溢出。

          內存泄露是哪個區域泄露?堆區,棧區是不會泄露的。

          window對象是DOM對象么?不是,window對象參與的循環引用不會內存泄露。

          內存泄露后果是什么?大多數時候后果不很嚴重,但過多DOM操作會導致網頁執行變慢。

          跳轉頁面后,內存泄露仍然存在么?仍然存在,直到關閉瀏覽器

          FireFox也會內存泄露么?FF2仍然有內存泄露

          posted on 2009-10-27 01:52 -274°C 閱讀(6323) 評論(3)  編輯  收藏 所屬分類: web前端


          FeedBack:
          # re: [總結轉載]JavaScript 內存泄露[未登錄]
          2010-07-05 17:14 | Adrian
          我曾經看到過這樣的說法,創建dom的時候,先創建子節點,當子節點完善后一次性添加到頁面中,不要一點點朝頁面上加東西,盡量減少document刷新次數,這樣效率會高點。(打個比方就是應該像 LeakMemory )可見這里我還是被某些書籍誤導了。至少他沒有告訴我內存泄露的問題。

          --
          是這么說的,不過是需要一個承接對象document.createDocumentFragment去appendChild,最后把這個fragment再append到targetDOM。

          這樣寫好像不會leak。。。
          var targetDOM = ...,
          fragment = ...;
          for {
          fragment.appendChild(nodes)
          }
          targetDOM.append(fragment)  回復  更多評論
            
          # re: [總結轉載]JavaScript 內存泄露
          2010-07-07 11:22 | 路人
          @Adrian
          MS的解釋是"插入順序不對",必須先將父級元素appendChild。這聽起來有些模糊,這里給出一個比較恰當的等價描述:永遠不要使用DOM節點樹之外元素的appendChild方法。

          會不會leak 跟瀏覽器本身關系比較大。

            回復  更多評論
            
          # re: [總結轉載]JavaScript 內存泄露
          2012-12-03 18:29 | nerd
          受用了!謝謝  回復  更多評論
            

          常用鏈接

          留言簿(21)

          隨筆分類(265)

          隨筆檔案(242)

          相冊

          JAVA網站

          關注的Blog

          搜索

          •  

          積分與排名

          • 積分 - 916109
          • 排名 - 40

          最新評論

          主站蜘蛛池模板: 绥滨县| 新兴县| 奎屯市| 怀安县| 得荣县| 罗城| 天津市| 乐业县| 江源县| 邵阳县| 黄大仙区| 景洪市| 休宁县| 靖江市| 长垣县| 韶关市| 宣威市| 屯昌县| 湄潭县| 孝昌县| 连城县| 西贡区| 佛教| 邻水| 仪征市| 海晏县| 通江县| 伊金霍洛旗| 泸水县| 桃江县| 耿马| 顺昌县| 峨山| 聊城市| 普兰县| 连江县| 肥城市| 南安市| 铅山县| 含山县| 嘉兴市|