文章出處

一、前言                            

  IE6~8除了不遵守W3C標準和各種詭異外,我想最讓人詬病的應該是內存泄露的問題了。這陣子趁項目技術調研的機會好好的再認識一回,以下內容若有紕漏請大家指正,謝謝!

  目錄一大坨!

    二、內存泄漏到底是哪里漏了?

       2.1. JS Engine Object、DOM Element 和 BOM Element

       2.2. JS Engine Object的內存回收機制

       2.3. DOM Element的內存回收機制

       2.4. 兩種泄漏方式

    三、4種泄漏模式

    3.1. Circular References

    3.2. Closures

    3.3. Cross-page Leaks

        3.4. Pseduo-Leaks

    四、當前頁面泄漏的示例

         4.1. DOM Hyperspace引起的DOM Element引用孤島

       4.2. 釋放Iframe沒那么簡單

    五、IE8下連續修改IMG的src居然耗盡內存?

    六、監控工具

    七、總結

    八、參考

 

二、內存泄漏到底是哪里漏了?                  

  SPA跑久了頁面響應速度劇減又被用戶投訴,搪塞說句“IE是比較容易發生內存泄漏,刷刷頁面就好”。那真的是刷刷頁面就能釋放泄漏了的內存嗎?下面我們一起來探討一下!

  內存泄漏:內存資源得不到釋放 && 失去對該內存區的指針 => 無法復用內存資源,最終導致內存溢出

  2.1. JS Engine Object、DOM Element 和 BOM Element

    Script中我們能操作的對象可分為三種:JS Engine Object、DOM Element 和 BOM Element。

       JS Engine Object: var obj = Object(); var array = [];等等 

     DOM Element: var el = document.createElement('div'); var div = document.getElementById('name');等等 

   BOM Element: window; window.location;等等 

       其中只有JS Engine Object和DOM Element是我們可以CRUD的,因此也就有可能發生內存泄漏的問題。

  2.2. JS Engine Object的內存回收機制 

   IE的JScript Garbage Collector采用的是Mark-and-Sweep算法,當執行垃圾回收時會先遍歷所有JS Engine Object并標記未被引用的對象,然后釋放掉被標記的內存空間。

   由于Mark-and-Sweep算法的緣故,也能很好地釋放引用孤島的內存空間。

   而IE下獨有的CollectGarbage()則用于回收無引用或引用孤島的JS Engine Object。

  2.3. DOM Element的內存回收機制

   當DOM Element不再被引用時會被回收,但具體被誰何時回收則有待研究了。

  2.4. 兩種泄漏方式

   a. 當前頁面泄漏:刷新頁面或跳轉到其他頁面就能釋放的內存資源。

   b. 跨頁面泄漏:刷新頁面或跳轉到其他頁面也無法釋放的內存資源。

   當前頁面泄漏處理難度相對簡單,跨頁面泄漏才是處理大頭。

 

三、4種泄漏模式                        

  下面是Justin Rogers總結出來的4種會引起泄漏的反模式。

  3.1. Circular References(導致跨頁面內存泄漏)

       循環引用可謂是引起內存泄漏的根本原因,其他的泄漏模式最底層還是因為出現的循環引用。   

               

Leak Memory

<div id="test"></div>
<script type="text/javascript">
  var $el = {tag: 'div', dom: null} // 創建JS Engine Object
  $el.dom = document.getElementById('test') // JS Engine Object references to DOM Element
  $el.dom.expandoProp = $el // DOM Element references to JS Engine Object

  // 造成circular references
  // GC不會清理$el,而頁面刷新時也不會清理$el.dom

  setTimeout('location.reload()', 500) // 刷新頁面
</script>

Non-Leak Memory

<body onunload="clearMemory()">
    <div id="test"></div>
    <script type="text/javascript">
      function clearMemory(){
        $el.dom.expandoProp = null; // 解除DOM Element references to JS Engine Object,那么頁面刷新時就會清除$el.dom,而$el也會被GC清除
      }

      var $el = {tag: 'div', dom: null} // 創建JS Engine Object
      $el.dom = document.getElementById('test') // JS Engine Object references to DOM Element
      $el.dom.expandoProp = $el // DOM Element references to JS Engine Object
    
      // 造成circular references
      // GC不會清理$el,而頁面刷新時也不會清理$el.dom
    
      setTimeout('location.reload()', 500) // 刷新頁面
    </script>
</body>

  3.2. Closures(導致跨頁面內存泄漏)

    閉包具有Lexical scope特性,延長了方法參數和局部變量的生命周期,但同時又容易在無意當中引入循環引用的問題。

Leak Memory

<div id="test"></div>
<script type="text/javascript">
  ;(function (){
    var $el = {tag: 'div', dom: null}
    $el.dom = document.getElementById('test') // JS Engine Object references to DOM Element
    $el.dom.attachEvent('click', onclick) // DOM Element references to JS Engine Object
    // 此時還沒形成circular references

    function onclick(){} // onclick的方法體內隱式引用$el及$el內的dom屬性,因此形成了circular refereneces
    // function onclick(){ return eval('$el && true || false') } 返回true
  }())
</script>

Non-Leak Memory

<div id="test"></div>
<script type="text/javascript">
  ;(function (){
    var $el = {tag: 'div', dom: null}
    $el.dom = document.getElementById('test') // JS Engine Object references to DOM Element
    $el.dom.attachEvent('click', onclick) // DOM Element references to JS Engine Object
    // 此時還沒形成circular references
  }())
  function onclick(){}  // onclick方法體內沒有引用$el
</script>

  3.3. Cross-page Leaks(當前頁面內存泄漏)

    由于節點建立聯系時會尋找scope,若沒有則創建temporary scope,若有則拋棄原有的temporary scope采用已有的scope。

    

Leak Memory

<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 ;
        } 
     </script>
     </head>
     <body>
         <button onclick ="LeakMemory()"> Memory Leaking Insert </button>
         <div id ="hostElement"></div>
     </body>
</html>

  當childDiv與parentDiv建立連接時,為讓childDiv能獲取parentDiv的信息,IE會創建temporary scope。而當將parentDiv添加到DOM tree中時,則childDiv和parentDiv均繼承document的scope,而temporary scope卻不會被GC釋放,而要等待瀏覽器刷新頁面才能清理。

Non-Leak Memory

<html>
     <head>
         <script language="JScript">
       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 ="CleanMemory()"> Clean Insert </button>
         <div id ="hostElement"></div>
     </body>
</html>

  一直使用document scope,不會創建temporary scope

  3.4. Pseduo-Leaks

    連續創建多個JS Engine Object,而GC未能及時釋放內存,其實根本就不是內存泄漏

var tmpStr
for(var i = 0; i < 100000; ++i) 
  tmpStr = "test"

 

四、當前頁面泄漏的示例                      

  4.1. DOM Hyperspace引起的DOM Element引用孤島

      DOM Hyperspace由PPK發現,在IE下通過removeChild或removeNode從父節點(無論是否已加入DOM Tree)中移除節點后,會創建一個新的#documentFragment,并且被移除的節點的parentNode為該#documentFragment,而該#documentFragment.firstChild為被移除的節點,因此存在DOM Element間的circular reference導致無法釋放,只有刷新頁面后才會釋放資源。

Leak Memory

var div = document.createElement('div')
document.body.appendChild(div)
div.parentNode.removeChild(div)

alert(div.parentNode) // IE8下為[Object object],Chrome等瀏覽器為null

Non-Leak Memory

function rm(el){
  if (!+'\v1'){
    var d = document.createElement('div')
    d.appendChild(el)
    d.innerHTML = ''
  }
  else{
    el.parentNode.removeChild(el)
  }
}

var div = document.createElement('div')
document.body.appendChild(div)
rm(div)

alert(div.parentNode) // IE8下為null

  4.2. 釋放Iframe沒那么簡單

      iframe所占的資源有兩部分:iframe元素所占的內存空間 和 iframe內頁面所占的內存空間。

      內存空間釋放步驟:

    1. 釋放 iframe內頁面所占的內存空間

      通過設置src=''或src='about:blank'來釋放內部頁面的資源

    2. 釋放 iframe元素所占的內存空間

      通過removeChild、removeNode等方法釋放iframe元素的內存空間

   ligerTab1.2.1的清除方式

var iframe = ...
iframe.src = 'about:blank'
iframe.contentWindow.document.write('')
CollectGarbage && CollectGarbage() 
iframe.parentNode.removeChild(iframe)

 

五、IE8下連續修改IMG的src居然耗盡內存?            

  由于IE8會對非原始尺寸的圖片進行抗鋸齒平滑處理,從而消耗更多的CPU和內存資源。當圖片大小和尺寸到一定時,則會出現掛死的情況。(IE6、7沒有抗鋸齒平滑處理,而IE9則移除該功能)

  而這種情況當然就不屬于Memory Leak啦!

  題外話:

     眾所周知IMG是replaced element,其width和height屬性缺省值又外部資源決定,而我們通過CSS設置的width和height屬性均是對缺省值的二次加工。

     假設圖片原始尺寸為width:200px/height:400px,現在通過CSS設置width:100px,那么圖片將按等比例縮放為width:100px/height:200px;但通過CSS設置width:100px/height:100px時,那么圖片則不是按等比例縮放了。

 

六、監控工具                            

  監控方式多種多樣,這里大概分為兩類:

  1. 當前頁面泄漏:Windows的任務管理器、Chrome->dev tools->Profiles->Take Heap Snapshot/Record Heap Allocations等等

  2. 跨頁面泄漏:sIEve

  

  操作步驟:

      1. 在Address輸入框輸入網址,點擊Go (瀏覽網頁)

      2. 執行測試用例

      3. 點擊about:blank按鈕(跳轉到空白頁)

      4. 查看#leaks列下是否有增長,有則表示出現跨頁面的內存泄漏

 

七、總結                             

    稍微小結一下:

      1. 單純的JS Engine Object的Circular References、Closures是不會引起內存泄漏;

      2. 單純的DOM Element的Circular References只會引起當前頁面的內存泄漏;

      3. JS Engine Object 和 DOM Element的Circular References、Closures會引起跨頁面的內存泄漏;

      4. 將DOM Element直接追加到DOM Tree中,可減少temporary scope的創建和丟棄;

      5. CollectGarbage()不是萬金油。

   上述內容以概念為主,最終還是要實戰來驗證和完善、補充。

   尊重原創,轉載請注明來自:^_^肥子John http://www.cnblogs.com/fsjohnhuang/p/4455822.html 

 

八、參考                             

  What are closures?

  Understanding and Solving Internet Explorer Leak Patterns

  JavaScript and memory leaks

 


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()