JavaScript 內(nèi)存泄露

          今天下午同事讓幫忙看web內(nèi)存泄露問題。當(dāng)時(shí)定位到創(chuàng)建ActiveX 對(duì)象的時(shí)候產(chǎn)生的,于是我對(duì)這個(gè)奇怪的問題進(jìn)行了一些深入探索。 

          很多時(shí)候我都依賴javascript的垃圾回收機(jī)制,所以對(duì)以及C++ 操作內(nèi)存語言常發(fā)生的內(nèi)存泄露是很陌生的。當(dāng)時(shí)創(chuàng)建回調(diào)函數(shù)用了閉包,當(dāng)然最終的解決方法是也避免閉包調(diào)用。 

              

              隨著這個(gè)問題的浮出水面,我回憶起以前的一個(gè)項(xiàng)目中也應(yīng)該存在這個(gè)內(nèi)存泄露問題。于是查閱了相關(guān)資料把類似的問題總結(jié)下來,希望對(duì)大家也有幫助。

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

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

          目前發(fā)現(xiàn)的可能導(dǎo)致內(nèi)存泄露的代碼有三種:

          · 循環(huán)引用

          · 自動(dòng)類型裝箱轉(zhuǎn)換

          · 某些DOM操作

          下面具體的來說說內(nèi)存是如何泄露的

          循環(huán)引用:這種方式存在于IE6FF2中(FF3未做測(cè)試),當(dāng)出現(xiàn)了一個(gè)含有DOM對(duì)象的循環(huán)引用時(shí),就會(huì)發(fā)生內(nèi)存泄露。

          什么是循環(huán)引用?首先搞清楚什么是引用,一個(gè)對(duì)象A的屬性被賦值為另一個(gè)對(duì)象B時(shí),則可以稱A引用了B。假如B也引用了A,那么AB之間構(gòu)成了循環(huán)引用。同樣道理 如果能找到A引用B B引用C C又引用A這樣一組飲用關(guān)系,那么這三個(gè)對(duì)象構(gòu)成了循環(huán)引用。當(dāng)一個(gè)對(duì)象引用自己時(shí),它自己形成了循環(huán)引用。注意,在js中變量永遠(yuǎn)是對(duì)象的屬性,它可以指向?qū)ο螅珱Q不是對(duì)象本身。

          循環(huán)引用很常見,而且通常是無害的,但如果循環(huán)引用中包含DOM對(duì)象或者ActiveX對(duì)象,那么就會(huì)發(fā)生內(nèi)存泄露。例子:

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

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

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

          OK, 讓我們來看看。假設(shè)A()執(zhí)行時(shí)創(chuàng)建的作用域?qū)ο蠼凶?/font>ScopeA 找到以下引用關(guān)系
          ScopeA引用DOM對(duì)象document.createElement("div");
          DOM對(duì)象document.createElement("div");引用函數(shù)function(){alert("hi")}
          函數(shù)function(){alert("hi")}引用ScopeA

          這樣就很清楚了,所謂closure泄露,只不過是幾個(gè)js特殊對(duì)象的循環(huán)引用而已。

          自動(dòng)類型裝箱轉(zhuǎn)換:這種泄露存在于ie6 ie7中。這是極其匪夷所思的一個(gè)bug,看下面代碼

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

          這段代碼怎么了?看看吧,"lalalalala"已經(jīng)泄露了。關(guān)鍵問題出在s.length上,我們知道js的類型中,string并非對(duì)象,但可以對(duì)它使用.運(yùn)算符,為什么呢?因?yàn)?/font>js的默認(rèn)類型轉(zhuǎn)換機(jī)制,允許js在遇到.運(yùn)算符時(shí)自動(dòng)將string轉(zhuǎn)換為object型中對(duì)應(yīng)的String對(duì)象。而這個(gè)轉(zhuǎn)換成的臨時(shí)對(duì)象100%會(huì)泄露(汗一下)。

          某些DOM操作也可能導(dǎo)致泄露 這些惡心的bug只存在于ie系列中。在ie7中 因?yàn)樵噲Dfix循環(huán)引用bug而讓情況變得更糟,以至于我對(duì)寫這一段種滿了恐懼。

          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>

          看看結(jié)果吧,LeakMemory造成了內(nèi)存泄露,而CleanMemory沒有,循環(huán)引用了么?仔細(xì)看看沒有。那么是什么問題呢?MS的解釋是"插入順序不對(duì)",必須先將父級(jí)元素appendChild。這聽起來有些模糊,這里給出一個(gè)比較恰當(dāng)?shù)牡葍r(jià)描述:永遠(yuǎn)不要使用DOM節(jié)點(diǎn)樹之外元素的appendChild方法

          我曾經(jīng)看到過這樣的說法,創(chuàng)建dom的時(shí)候,先創(chuàng)建子節(jié)點(diǎn),當(dāng)子節(jié)點(diǎn)完善后一次性添加到頁面中,不要一點(diǎn)點(diǎn)朝頁面上加?xùn)|西,盡量減少document刷新次數(shù),這樣效率會(huì)高點(diǎn)。(打個(gè)比方就是應(yīng)該像 LeakMemory )可見這里我還是被某些書籍誤導(dǎo)了。至少他沒有告訴我內(nèi)存泄露的問題。

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

          接下來談?wù)?font face="Arial">ie7的這個(gè)設(shè)計(jì)吧,坦白的說,這種做法純粹是偷懶的垃圾做法。動(dòng)態(tài)垃圾回收不是保證所有內(nèi)存都在離開頁面時(shí)收回,而是要保證內(nèi)存的充分利用,運(yùn)行時(shí)不回收,等到離開時(shí)回收有什么用?這只是名義上的避免泄露,其實(shí)是完全的泄露。況且還沒有回收DOM節(jié)點(diǎn)樹之外的元素。

           4.內(nèi)存泄露的解決方案

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

          事實(shí)上,通過一些很簡(jiǎn)單的小技巧,可以巧妙的繞開這些危險(xiǎn)的bug

          to be continued......

          coming soon:

          · 顯式類型轉(zhuǎn)換

          · 避免事件導(dǎo)致的循環(huán)引用

          · 不影響返回值地打破循環(huán)引用

          · 延遲appendChild

          · 代理DOM對(duì)象

          · 顯式類型轉(zhuǎn)換

          首先說說最容易處理的情況 對(duì)于類型轉(zhuǎn)換造成的錯(cuò)誤,我們可以通過顯式類型轉(zhuǎn)換來避免:

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

           這個(gè)太容易了,算不上正經(jīng)方案。不過類型轉(zhuǎn)換泄露也就這一種處理方法了。

          · 避免事件導(dǎo)致的循環(huán)引用

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

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

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

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

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

          解決方案1

          構(gòu)造一個(gè)不含a的新環(huán)境

          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訪問,將其它需要訪問的外層函數(shù)變量傳遞給BuildEvent就可以了。保持BuildEvent定義和調(diào)用的參數(shù)名一致,會(huì)帶來方便。

          解決方案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之后執(zhí)行,如果finall塊不返回值,才會(huì)返回try塊的返回值。

          · 延遲appendChild

          還記得函數(shù)的lazy initalize吧,對(duì)于ie惡心至極的DOM操作泄露,我們需要用類似的方法去處理。在一個(gè)函數(shù)中構(gòu)造一個(gè)復(fù)雜對(duì)象,在需要的時(shí)候?qū)⒅?/font>appendChildDOM樹上,這是很常見的做法,但在IE6中,這樣做將導(dǎo)致所謂的"插入順序內(nèi)存泄露",沒有別的辦法,我們只能用一個(gè)數(shù)組parts保存子節(jié)點(diǎn),編寫一個(gè)appendTo方法先序遍歷節(jié)點(diǎn)樹,去把它掛在某個(gè)DOM節(jié)點(diǎn)上。

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

           

          · 垃圾箱

          對(duì)于ie7,我比較無可奈何,因?yàn)?/font>DOM對(duì)象不會(huì)被CG程序回收,只有離開頁面時(shí)會(huì)被回收,所以我的建議是:使用DOM要有節(jié)制,盡量多用innerHTML...... good luck.

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

          · 代理對(duì)象

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

          5 .FAQ

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

          程序中提示,內(nèi)存不足,是內(nèi)存泄露么?不是,這一般是無限遞歸函數(shù)調(diào)用導(dǎo)致棧內(nèi)存溢出。

          內(nèi)存泄露是哪個(gè)區(qū)域泄露?堆區(qū),棧區(qū)是不會(huì)泄露的。

          window對(duì)象是DOM對(duì)象么?不是,window對(duì)象參與的循環(huán)引用不會(huì)內(nèi)存泄露。

          內(nèi)存泄露后果是什么?大多數(shù)時(shí)候后果不很嚴(yán)重,但過多DOM操作會(huì)導(dǎo)致網(wǎng)頁執(zhí)行變慢。

          跳轉(zhuǎn)頁面后,內(nèi)存泄露仍然存在么?仍然存在,直到關(guān)閉瀏覽器

          FireFox也會(huì)內(nèi)存泄露么?FF2仍然有內(nèi)存泄露

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


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

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

          這樣寫好像不會(huì)leak。。。
          var targetDOM = ...,
          fragment = ...;
          for {
          fragment.appendChild(nodes)
          }
          targetDOM.append(fragment)  回復(fù)  更多評(píng)論
            
          # re: [總結(jié)轉(zhuǎn)載]JavaScript 內(nèi)存泄露
          2010-07-07 11:22 | 路人
          @Adrian
          MS的解釋是"插入順序不對(duì)",必須先將父級(jí)元素appendChild。這聽起來有些模糊,這里給出一個(gè)比較恰當(dāng)?shù)牡葍r(jià)描述:永遠(yuǎn)不要使用DOM節(jié)點(diǎn)樹之外元素的appendChild方法。

          會(huì)不會(huì)leak 跟瀏覽器本身關(guān)系比較大。

            回復(fù)  更多評(píng)論
            
          # re: [總結(jié)轉(zhuǎn)載]JavaScript 內(nèi)存泄露
          2012-12-03 18:29 | nerd
          受用了!謝謝  回復(fù)  更多評(píng)論
            

          常用鏈接

          留言簿(21)

          隨筆分類(265)

          隨筆檔案(242)

          相冊(cè)

          JAVA網(wǎng)站

          關(guān)注的Blog

          搜索

          •  

          積分與排名

          • 積分 - 914103
          • 排名 - 40

          最新評(píng)論

          主站蜘蛛池模板: 广饶县| 广东省| 财经| 莱西市| 临高县| 阳春市| 禹城市| 安国市| 乐昌市| 襄垣县| 太和县| 汾阳市| 贵溪市| 延庆县| 抚松县| 隆安县| 安塞县| 中西区| 永清县| 新野县| 大冶市| 高台县| 当雄县| 黄龙县| 金沙县| 江永县| 莱阳市| 灌云县| 阿克陶县| 新疆| 墨脱县| 双峰县| 鄂托克前旗| 郑州市| 香港| 本溪| 浦城县| 雷州市| 宁晋县| 上林县| 通渭县|