js內存深刻學習(二)

繼上一篇文章 js內存深刻學習(一)

3. 內存泄漏

對於持續運行的服務進程(daemon),必須及時釋放再也不用到的內存。不然,內存佔用愈來愈高,輕則影響系統性能,重則致使進程崩潰。 對於再也不用到的內存,沒有及時釋放,就叫作內存泄漏(memory leak)html

3.1 node.js內存補充

node.js中V8中的內存分代:node

  • 新生代:存活時間較短的對象,會被GC自動回收的對象及做用域,好比不被引用的對象及調用完畢的函數等。
  • 老生代:存活時間較長或常駐內存的對象,好比閉包由於外部仍在引用內部做用域的變量而不會被自動回收,故會被放在常駐內存中,這種就屬於在新生代中持續存活,因此被移到了老生代中,還有一些核心模塊也會被存在老生代中,例如文件系統(fs)、加密模塊(crypto)等
  • 如何調整內存分配大小:
    • 啓動node進程時添加參數便可 node --max-old-space-size=1700 <project-name>.js 調整老生代內存限制,單位爲MB(貌似最高也只能1.8G的樣子)(老生代默認限制爲 64/32 位 => 1400/700 MB)git

    • node --max-new-space-size=1024 <project-name>.js 調整新生代內存限制,單位爲KB(老生代默認限制爲 64/32 位 => 32/16 MB) 接!github

內存回收時使用的算法:算法

  • Scavenge 算法(用於新生代,具體實現中採用 Cheney 算法)chrome

    • 算法的結果通常只有兩種,空間換時間或時間換空間,Cheney屬於前者
    • 它將現有的空間分半,一個做爲 To 空間,一個做爲 From 空間,當開始垃圾回收時會檢查 from 空間中存活的對象並賦複製入 To 空間中,而非存活就會被直接釋放,完成複製後,二者職責互換,下一輪迴收時重複操做,也就是說咱們本質上只使用了一半的空間,明顯放在老生代這麼大的內存浪費一半就很不合適,並且老生代通常生命週期較長,須要複製的對象過多,正所以因此它就被用於新生代中,新生代的生命週期短,通常不會有這麼大的空間須要留存,相對來講這是效率最高的選擇,剛和適合這個算法
    • 前面咱們提到過,若是對象存活時間較長或較大就會重新生代移到老生代中,那麼何種條件下會過渡呢,知足如下2個條件中的一個就會被過渡
      • 在一次 from => to 的過程當中已經經歷過一次 Scavenge 回收,即通過一次新生代回收後,再下次回收時仍然存在,此時這個對象將會從本次的 from 中直接複製到老生代中,不然則正常複製到 To
      • from => to 時,佔用 to 的空間達到 25% 時,將會因爲空間使用過大自動晉升到老生代中
  • Mark-Sweep & Mark-Compact(用於老生代的回收算法)數組

    • 新生代的最後咱們提到過,Cheney 會浪費一半的空間,這個缺點在老生代是不可原諒的,畢竟老生代有 1.4G 不是,浪費一半就是 700M 啊,並且每次都去複製這麼多常駐對象,簡直浪費,因此咱們是不可能繼續採納 Scavenge 的;
    • mark-sweep 顧名思義,標記清除,上一條咱們提到過,咱們要杜絕大量複製的狀況,由於大部分都是常駐對象,因此 mark-sweep 只會標記死去的老對象,並將其清除,不會去作複製的行爲,由於死對象在老生代中佔比是很低的,但此時咱們很明顯看到它的缺點就是清除死去的部分後,可能會形成內存的不連續而在下次分配大對象前馬上先觸發回收,可是其實須要回收的那些在上輪已經被清除了,只是沒有將活着的對象連續起來 。缺點舉例:這就像 buffer 同樣,在一段 buffer 中,咱們清除了其中斷斷續續的部分,這些部分就爲空了,可是剩下的部分會變得不連續,下次咱們分配大對象進來時,大對象是一個總體,咱們不可能將其打散分別插入本來斷斷續續的空間中,不然將變的不連續,下次咱們去調用這個大對象時也將變得不連續,這就沒有意義了,這就像你將一我的要塞進一個已經裝滿了傢俱的房間裏同樣,各個傢俱間可能會存在空隙,可是你一個總體的人怎麼可能打散分散到這些空間?並在下次調用時在拼到一塊兒呢(什麼納米單位的別來槓,你能夠本身想其餘例子)
    • 在這個缺點的基礎上,咱們使用了 mark-compact 來解決,它會在 mark-sweep 標記死亡對象後,將活着的對象所有向一側移動,移動完成後,一側全爲生,一側全爲死,此時咱們即可以直接將死的一側直接清理,下次分配大對象時,直接從那側拼接上便可,彷彿就像把傢俱變成工整了,將一些沒用的小傢俱整理到一側,將有用的其餘傢俱所有工整擺放,在下次有新傢俱時,將一側的小傢俱所有丟掉,在將新的放到有用的旁邊緊密結合。

buffer 聲明的都爲堆外內存,它們是由系統限定而非 V8 限定,直接由 C++ 進行垃圾回收處理,而不是 V8,在進行網絡流與文件 I/O 的處理時,buffer 明顯知足它們的業務需求,而直接處理字符串的方式,顯然在處理大文件時有心無力。因此由 V8 處理的都爲堆內內存。瀏覽器

3.2 識別方法

一、瀏覽器方法緩存

  • 打開開發者工具,選擇 Memory
  • 在右側的Select profiling type字段裏面勾選 timeline
  • 點擊左上角的錄製按鈕。
  • 在頁面上進行各類操做,模擬用戶的使用狀況。
  • 一段時間後,點擊左上角的 stop 按鈕,面板上就會顯示這段時間的內存佔用狀況。

二、命令行方法 使用 Node 提供的 process.memoryUsage 方法。網絡

console.log(process.memoryUsage());
// 輸出
{ 
  rss: 27709440,		// resident set size,全部內存佔用,包括指令區和堆棧
  heapTotal: 5685248,   // "堆"佔用的內存,包括用到的和沒用到的
  heapUsed: 3449392,	// 用到的堆的部分
  external: 8772 		// V8 引擎內部的 C++ 對象佔用的內存
}

判斷內存泄漏,以heapUsed字段爲準。

3.3 常見內存泄露場景

  • 意外的全局變量

    function foo(arg) {
        bar = "this is a hidden global variable"; // winodw.bar = ...
    }
    或者
    
    function foo() {
            this.variable = "potential accidental global";
        }
        
        // Foo 調用本身,this 指向了全局對象(window)
        // 而不是 undefined
        foo();

    解決方法: 在 JavaScript 文件頭部加上 'use strict',使用嚴格模式避免意外的全局變量,此時上例中的this指向undefined。

    儘管咱們討論了一些意外的全局變量,可是仍有一些明確的全局變量產生的垃圾。它們被定義爲不可回收(除非定義爲空或從新分配)。尤爲當全局變量用於臨時存儲和處理大量信息時,須要多加當心。若是必須使用全局變量存儲大量數據時,確保用完之後把它設置爲 null 或者從新定義。與全局變量相關的增長內存消耗的一個主因是緩存。緩存數據是爲了重用,緩存必須有一個大小上限纔有用。高內存消耗致使緩存突破上限,由於緩存內容沒法被回收。

  • 被遺忘的計時器或回調函數

    如計時器的使用:

    var someResource = getData();
        setInterval(function() {
            var node = document.getElementById('Node');
            if(node) {
                // 處理 node 和 someResource
                node.innerHTML = JSON.stringify(someResource));
            }
        }, 1000);

    定義了一個someResource變量,變量在計時器setInterval內部一直被引用着,成爲一個閉包使用,即便移除了Node節點,因爲計時器setInterval沒有中止。其內部仍是有對someResource的引用,因此v8不會釋放someResource變量的。

    var element = document.getElementById('button');
        function onClick(event) {
            element.innerHTML = 'text';
        }
        
        element.addEventListener('click', onClick);

    對於上面觀察者的例子,一旦它們再也不須要(或者關聯的對象變成不可達),明確地移除它們很是重要。老的 IE 6 是沒法處理循環引用的。由於老版本的 IE 是沒法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會致使內存泄漏。

    可是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法(標記清除),已經能夠正確檢測和處理循環引用 了。即回收節點內存時,沒必要非要調用 removeEventListener 了。(不是很理解)

  • 對DOM 的額外引用

    若是把DOM 存成字典(JSON 鍵值對)或者數組,此時,同一個 DOM 元素存在兩個引用:一個在 DOM 樹中,另外一個在字典中。若是要回收該DOM元素內存,須要同時清除掉這兩個引用。

    var elements = {
            button: document.getElementById('button'),
            image: document.getElementById('image'),
            text: document.getElementById('text')
        };
        
        document.body.removeChild(document.getElementById('button'));
        // 此時,仍舊存在一個全局的 #button 的引用(在elements裏面)。button 元素仍舊在內存中,不能被回收。

    若是代碼中保存了表格某一個 <td> 的引用。未來決定刪除整個表格的時候,咱們覺得 GC 會回收除了已保存的 <td> 之外的其它節點。實際狀況並不是如此:此 <td> 是表格的子節點,子元素與父元素是引用關係。因爲代碼保留了 <td> 的引用,致使整個表格仍待在內存中。

    因此保存 DOM 元素引用的時候,要當心謹慎。

  • 閉包

    var theThing = null;
        var replaceThing = function () {
          var originalThing = theThing;
          var unused = function () {
            if (originalThing)
              console.log("hi");
          };
          theThing = {
            longStr: new Array(1000000).join('*'),
            someMethod: function () {
              console.log(someMessage);
            }
          };
        };
        setInterval(replaceThing, 1000);

    代碼片斷作了一件事情:每次調用 replaceThing ,theThing 獲得一個包含一個大數組和一個新閉包(someMethod)的新對象。同時,變量 unused 是一個引用 originalThing 的閉包(先前的 replaceThing 又調用了 theThing )。思緒混亂了嗎?最重要的事情是,閉包的做用域一旦建立,它們有一樣的父級做用域,做用域是共享的。someMethod 能夠經過 theThing 使用,someMethod 與 unused 分享閉包做用域,儘管 unused 從未使用,它引用的 originalThing 迫使它保留在內存中(防止被回收)。當這段代碼反覆運行,就會看到內存佔用不斷上升(新建的多個originalThing一直被保存在內存中),垃圾回收器(GC)並沒有法下降內存佔用。本質上,閉包的鏈表已經建立,每個閉包做用域攜帶一個指向大數組的間接的引用,形成嚴重的內存泄漏。

    這時候應該在 replaceThing 的最後添加 originalThing = null,主動解除對象引用。

3.4 具體例子

timeline 標籤擅長作這些。在 Chrome 中打開例子,打開 Dev Tools ,切換到 timeline,勾選 memory 並點擊記錄按鈕,而後點擊頁面上的 The Button 按鈕。過一陣中止記錄看結果:

Chrome

兩種跡象顯示出現了內存泄漏,圖中的 Nodes(綠線)和 JS heap(藍線)。Nodes 穩定增加,並未降低,這是個顯著的信號。

JS heap 的內存佔用也是穩定增加。因爲垃圾收集器的影響,並不那麼容易發現。圖中顯示內存佔用忽漲忽跌,實際上每一次下跌以後,JS heap 的大小都比原先大了。換言之,儘管垃圾收集器不斷的收集內存,內存仍是週期性的泄漏了。

參考文章

https://github.com/yygmind/blog

http://www.cnblogs.com/vajoy/p/3703859.html

https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/

https://blog.csdn.net/yolo0927/article/details/80471220

相關文章
相關標籤/搜索