1. 程式人生 > >JavaScript如何工作:垃圾回收機制 + 常見的4種記憶體洩漏

JavaScript如何工作:垃圾回收機制 + 常見的4種記憶體洩漏

原文地址: How JavaScript works: memory management + how to handle 4 common memory leaks

本文永久連結: https://didiheng.com/front/2019-04-01.html

有部分的刪減和修改,不過大部分是參照原文來的,翻譯的目的主要是弄清JavaScript的垃圾回收機制,覺得有問題的歡迎指正。

#JavaScript 中的記憶體分配

現在我們將解釋第一步(分配記憶體)是如何在JavaScript中工作的。

JavaScript 減輕了開發人員處理記憶體分配的責任 - JavaScript自己執行了記憶體分配,同時聲明瞭值。

var n = 374; // 為number分配記憶體
var s = 'sessionstack'; // 為string分配記憶體  
var o = {
  a: 1,
  b: null
}; //為物件及屬性分配記憶體 

function f(a) {
  return a + 3;
} // 為函式分配記憶體
// 函式表示式分配記憶體
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

#在 JavaScript 中使用記憶體

基本上在 JavaScript 中使用分配的記憶體,意味著在其中讀寫。

這可以通過讀取或寫入變數或物件屬性的值,甚至傳遞一個變數給函式來完成。

#垃圾回收機制

由於發現一些記憶體是否“不再需要”事實上是不可判定的,所以垃圾收集在實施一般問題解決方案時具有侷限性。下面將解釋主要垃圾收集演算法及其侷限性的基本概念。

#記憶體引用

如果一個物件可以訪問另一個物件(可以是隱式的或顯式的),則稱該物件引用另一個物件。例如, 一個 JavaScript 引用了它的 prototype (隱式引用)和它的屬性值(顯式引用)。

在這種情況下,“物件”的概念擴充套件到比普通JavaScript物件更廣泛的範圍,幷包含函式作用域(或全域性詞法範圍)。

詞法作用域定義了變數名如何在巢狀函式中解析:即使父函式已經返回,內部函式仍包含父函式的作用域。

#引用計數垃圾收集

這是最簡單的垃圾收集演算法。 如果有零個指向它的引用,則該物件被認為是“可垃圾回收的”。 請看下面的程式碼:

var o1 = {
  o2: {
    x: 1
  }
};
// 兩個物件被建立。
// ‘o1’物件引用‘o2’物件作為其屬性。
// 不可以被垃圾收集

var o3 = o1; // ‘o3’變數是第二個引用‘o1‘指向的物件的變數. 
                                                       
o1 = 1;      // 現在,在‘o1’中的物件只有一個引用,由‘o3’變量表示

var o4 = o3.o2; // 物件的‘o2’屬性的引用.
                // 此物件現在有兩個引用:一個作為屬性、另一個作為’o4‘變數

o3 = '374'; // 原來在“o1”中的物件現在為零,對它的引用可以垃圾收集。
            // 但是,它的‘o2’屬性存在,由‘o4’變數引用,因此不能被釋放。

o4 = null; // ‘o1’中最初物件的‘o2’屬性對它的引用為零。它可以被垃圾收集。

#週期產生問題

在週期迴圈中有一個限制。在下面的例子中,兩個物件被建立並相互引用,這就建立了一個迴圈。在函式呼叫之後,它們會超出界限,所以它們實際上是無用的,並且可以被釋放。然而,引用計數演算法認為,由於兩個物件中的每一個都被至少引用了一次,所以兩者都不能被垃圾收集。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // ‘o1’ 應用 ‘02’ o1 references o2
  o2.p = o1; // ‘o2’ 引用 ‘o2’ . 一個迴圈被建立
}
f();

#標記和掃描演算法

為了確定是否需要某個物件,本演算法判斷該物件是否可訪問。

標記和掃描演算法經過這 3 個步驟:

1.根節點:一般來說,根是程式碼中引用的全域性變數。例如,在 JavaScript 中,可以充當根節點的全域性變數是“window”物件。Node.js 中的全域性物件被稱為“global”。完整的根節點列表由垃圾收集器構建。

2.然後演算法檢查所有根節點和他們的子節點並且把他們標記為活躍的(意思是他們不是垃圾)。任何根節點不能訪問的變數將被標記為垃圾。

3.最後,垃圾收集器釋放所有未被標記為活躍的記憶體塊,並將這些記憶體返回給作業系統。

標記和掃描演算法行為的視覺化。

因為“一個物件有零引用”導致該物件不可達,所以這個演算法比前一個演算法更好。我們在週期中看到的情形恰巧相反,是不正確的。 截至 2012 年,所有現代瀏覽器都內建了標記掃描式的垃圾回收器。去年在 JavaScript 垃圾收集(通用/增量/併發/並行垃圾收集)領域中所做的所有改進都是基於這種演算法(標記和掃描)的實現改進,但這不是對垃圾收集演算法本身的改進,也不是對判斷一個物件是否可訪問這個目標的改進。

#週期不再是問題

在上面的例子中,函式呼叫返回後,兩個物件不再被全域性物件中的變數引用。因此,垃圾收集器會認為它們不可訪問。

即使兩個物件之間有引用,根節點它們不在被訪問。

#統計垃圾收集器的直觀行為

儘管垃圾收集器很方便,但他們也有自己的一套策略。其中之一是不確定性。換句話說,GC(垃圾收集器)是不可預測的。你不能確定一個垃圾收集器何時會執行收集。這意味著在某些情況下,程式其實需要更多的記憶體。其他情況下,在特別敏感的應用程式中,短暫和卡頓可能是明顯的。儘管不確定性意味著不能確定一個垃圾收集器何時執行收集,大多數 GC 共享分配中的垃圾收集通用模式。如果沒有執行分配,大多數 GC 保持空閒狀態。考慮如下場景:

1.大量的分配被執行。

2.大多數這些元素(或全部)被標記為不可訪問(假設我們廢除一個指向我們不再需要的快取的引用)。

3.沒有執行更深的記憶體分配。

在這種情況下,大多數 GC 不會執行任何更深層次的收集。換句話說,即使存在引用可用於收集,收集器也不會收集這些引用。這些並不是嚴格的洩漏,但仍會導致高於日常的記憶體使用率。

#什麼是記憶體洩漏?

記憶體洩漏是應用程式過去使用,但不再需要的尚未返回到作業系統或可用記憶體池的記憶體片段。由於沒有被釋放而導致的,它將可能引起程式的卡頓和崩潰。

#JavaScript 常見的四種記憶體洩漏

#1:全域性變數

function foo(arg) {
    bar = "some text";
    // window.bar = "some text";
}

假設 bar 的目的只是引用 foo 函式中的一個變數。然而不使用 var 來宣告它,就會建立一個冗餘的全域性變數。

你可以通過在 JavaScript 檔案的開頭新增 'use strict'; 來避免這些後果,這將開啟一種更嚴格的 JavaScript 解析模式,從而防止意外建立全域性變數。

意外的全域性變數當然是個問題,然而更常出現的情況是,你的程式碼會受到顯式的全域性變數的影響,而這些全域性變數無法通過垃圾收集器收集。需要特別注意用於臨時儲存和處理大量資訊的全域性變數。如果你必須使用全域性變數來儲存資料,當你這樣做的時候,要保證一旦完成使用就把他們賦值為 null 或重新賦值 。

#2:被忘記的定時器或者回調函式

我們以經常在 JavaScript 中使用的 setInterval 為例。

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每5秒執行一次.

上面的程式碼片段顯示了使用定時器引用節點或無用資料的後果。它既不會被收集,也不會被釋放。無法被垃圾收集器收集,頻繁的被呼叫,佔用記憶體。

而正確的使用方法是,確保一旦依賴於它們的事件已經處理完成,就通過明確的呼叫來刪除它們。

#3:閉包

閉包是JavaScript開發的一個關鍵點:一個內部函式可以訪問外部(封閉)函式的變數。

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

一旦呼叫了 replaceThing 函式,theThing 就得到一個新的物件,它由一個大陣列和一個新的閉包(someMethod)組成。然而 originalThing 被一個由 unused 變數(這是從前一次呼叫 replaceThing 變數的 Thing 變數)所持有的閉包所引用。需要記住的是一旦為同一個父作用域內的閉包建立作用域,作用域將被共享。

在個例子中,someMethod 建立的作用域與 unused 共享。unused 包含一個關於 originalThing 的引用。即使 unused 從未被引用過,someMethod 也可以通過 replaceThing 作用域之外的 theThing 來使用它(例如全域性的某個地方)。由於 someMethod 與 unused 共享閉包範圍,unused 指向 originalThing 的引用強制它保持活動狀態(兩個閉包之間的整個共享範圍)。這阻止了它們的垃圾收集。

在上面的例子中,為閉包 someMethod 建立的作用域與 unused 共享,而 unused 又引用 originalThing。someMethod 可以通過 replaceThing 範圍之外的 theThing 來引用,儘管 unused 從來沒有被引用過。事實上,unused 對 originalThing 的引用要求它保持活躍,因為 someMethod 與 unused 的共享封閉範圍。

所有這些都可能導致大量的記憶體洩漏。當上面的程式碼片段一遍又一遍地執行時,您可以預期到記憶體使用率的上升。當垃圾收集器執行時,其大小不會縮小。一個閉包鏈被建立(在例子中它的根就是 theThing 變數),並且每個閉包作用域都包含對大陣列的間接引用。

#4: DOM 的過度引用

有些情況下開發人員在變數中儲存 DOM 節點。假設你想快速更新表格中幾行的內容。如果在字典或陣列中儲存對每個 DOM 行的引用,就會產生兩個對同一個 DOM 元素的引用:一個在 DOM 樹中,另一個在字典中。如果你決定刪除這些行,你需要記住讓兩個引用都無法訪問。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    // image 元素是body的直接子元素。
    document.body.removeChild(document.getElementById('image'));
    // 我們仍然可以在全域性元素物件中引用button。換句話說,button元素仍在記憶體中,無法由GC收集
}

在涉及 DOM 樹內的內部節點或子節點時,還有一個額外的因素需要考慮。如果你在程式碼中保留對table表格單元格(td 標記)的引用,並決定從 DOM 中刪除該table表格但保留對該特定單元格td的引用,則可以預見到嚴重的記憶體洩漏。你可能會認為垃圾收集器會釋放除了那個單元格td之外的所有東西。但情況並非如此。由於單元格td是table表格的子節點,並且子節點保持對父節點的引用,所以對table表格對單元格td的這種單引用會把整個table表格儲存在記憶體中。

我們在 SessionStack 嘗試遵循這些最佳實踐,編寫正確處理記憶體分配的程式碼,原因如下:

一旦將 SessionStack 整合到你的生產環境的 Web 應用程式中,它就會開始記錄所有的事情:所有的 DOM 更改,使用者互動,JavaScript 異常,堆疊跟蹤,失敗網路請求,除錯訊息等。

通過 SessionStack web 應用程式中的問題,並檢視所有的使用者行為。所有這些都必須在您的網路應用程式沒有效能影響的情況下進行。

由於使用者可以重新載入頁面或導航你的應用程式,所有的觀察者,攔截器,變數分配等都必須正確處理,這樣它們才不會導致任何記憶體洩漏,也不會增加我們正在整合的Web應用程式的記憶體消耗。

這裡有一個免費的計劃所以你可以試試看.

#Resources

How JavaScript works: memory management + how to handle 4 common memory leaks

MDN 記憶體管理

ps: 順便推一下自己的個人公眾號:Yopai,有興趣的可以關注,每週不定期更新,分享可以增加世界的快樂

相關推薦

JavaScript如何工作垃圾回收機制 + 常見4記憶體洩漏

原文地址: How JavaScript works: memory management + how to handle 4 common memory leaks 本文永久連結: https://didiheng.com/front/2019-04-01.html 有部分的刪減和修改,不過大部分

python3垃圾回收機制

1.垃圾回收機制演算法採用的是 引用計數 直接給概念,小夥伴很難理解,我用一個例子保證你get到。 a=[1,2,3] #宣告變數後 引用計數 +1 b=a #增加一個引用, 引用計數 +1 c=b #增加一個引用, 引用計數 +1 當我們刪除 del a #刪

深入理解JVM虛擬機器(二)垃圾回收機制

談起GC,應該是讓Java程式設計師最激動的一項技術,我相信每個Java程式設計師都有探究GC本質的衝動!JVM垃圾回收機制對於瞭解物件的建立和物件的回收極為重要,是每個Java程式設計師必須掌握的技能。 本部落格圍繞三個問題來展開 哪些記憶體需要回收? 什

Java 垃圾回收機制與幾垃圾回收演算法

一、如何確定某個物件是“垃圾”? 這一小節先了解一個最基本的問題:如果確定某個物件是“垃圾”?既然垃圾收集器的任務是回收垃圾物件所佔的空間供新的物件使用,那麼垃圾收集器如何確定某個物件是“垃圾”?通過什麼方法判斷一個物件可以被回收了。 在java中是通過引用來和物件進行關

第三章垃圾回收垃圾回收器的兩算法

是否 div 搜索路徑 word position 其他 範圍 ava 對象實例 垃圾回收需要考慮三個問題: 哪些內存需要回收? 什麽時候回收? 如何回收? JVM中程序計數器、虛擬機棧、本地方法三個區域隨線程而生,隨線程而死,這三個區域的內存分配和回收都具有

深入淺出Java垃圾回收機制

但是 enter 相關 html 帶來 生命周期 不同 追蹤 lee 原文鏈接:http://www.importnew.com/1993.html 對於Java開發人員來說,了解垃圾回收機制(GC)有哪些好處呢?首先可以滿足作為一名軟件工程師的求知欲,其次,深入了解GC如

Java分代垃圾回收機制年輕代/年老代/持久代(轉)

進行 目標 targe 先後 技術分享 靜態文件 運行 you 頻繁 虛擬機中的共劃分為三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,

JavaScript垃圾回收機制

垃圾回收器 聲明 過程 賦值 cti 變量 global light 垃圾回收 原文   https://www.jianshu.com/p/4aa1a29781cc 大綱   1、認識垃圾回收機制  2、垃圾回收機制的原理  3、垃圾回收機制的標記策略  4、垃圾回收機制

JavaScript基礎概念之----垃圾回收機制

內存空間 工作 清除 UNC var span javascrip 去掉 似的 分為兩種: 標記清除 引用計數 標記清除 當變量進入環境時,就將這個變量標記為“進入環境”。當變量離開環境時,則將其標記為“離開環境”。 垃圾收集器在運行的時候會給存儲在內存中的所有變量都加

JVM系列第8講JVM 垃圾回收機制

在第 6 講中我們說到 Java 虛擬機器的記憶體結構,提到了這部分的規範其實是由《Java 虛擬機器規範》指定的,每個 Java 虛擬機器可能都有不同的實現。其實涉及到 Java 虛擬機器的記憶體,就不得不談到 Java 虛擬機器的垃圾回收機制。因為記憶體總是有限的,我們需要一個機制來不斷地回收廢棄的記憶體

要想學好Java程式設計,構造器、方法過載、this關鍵字、垃圾回收機制,這4關一定要過!

有人說,你應該關注時事、財經,甚至流行的電影、電視劇,才有可能趁著熱點寫出爆文;有人說,你別再寫“無聊”的技術文了,因為程式設計師的圈子真的很小,即便是像鴻祥那樣的招牌大牛,文章是那麼的乾貨,瀏覽量有多少?不到萬吧;有人說,你別妄想在寫作上面知識變現了,因為你寫的文章真的很不優秀,我都不愛看! 我想說,你們

要想學好Java編程,構造器、方法重載、this關鍵字、垃圾回收機制,這4關一定要過!

社會 tor 沒有 type 遇到 一個 結果 回收 爆笑 有人說,你應該關註時事、財經,甚至流行的電影、電視劇,才有可能趁著熱點寫出爆文;有人說,你別再寫“無聊”的技術文了,因為程序員的圈子真的很小,即便是像鴻洋那樣的招牌大牛,文章是那麽的幹貨,瀏覽量有多少?不到萬吧;有

JavaJVM垃圾回收機制

JVM垃圾回收機制 提到Java垃圾回收機制就不得不提到一個方法: system.gc() 用於呼叫垃圾收集器,在呼叫時垃圾收集器將執行以回收未使用的記憶體空間,它將嘗試釋放被丟棄物件所佔用的空間。 作為程式設計師有必要了解gc方

javascript垃圾回收機制

javascript是一門具有自動垃圾收集機制的程式語言,開發人員不必關心記憶體分配和回收問題。這種垃圾收集機制的原理就是:找出那些不再繼續使用的變數,然後釋放其佔用的記憶體。在javascript中垃圾回收機制有兩種,標記清除和引用計數,和java類似。 垃圾收集機制的原

JAVA虛擬機器之一垃圾回收(GC)機制

引言 java對於其它語言(c/c++)來說,建立一個物件使用後,不用顯式的delete/free,且能在一定程度上保證系統記憶體資源及時回收,這要功歸於java的自動垃圾回收機制(Garbage Collection,GC),但也是因為自動回收機制存在,一旦系統內洩漏或存

JVM的4垃圾回收演算法、垃圾回收機制與總結

本文標題:直通BAT必考題系列:JVM的4種垃圾回收演算法、垃圾回收機制與總結  轉載請保留頁面地址:http://youzhixueyuan.com/jvm-garbage-collection-algorithm.html 垃圾回收演算法 1.標記清除 標記-清除演算

Java效能優化三記憶體管理與垃圾回收機制,開發必備優化技巧!

一、Java 類載入機制的特點: (1)基於父類的委託機制:執行一個程式時,總是由 AppClass Loader (系統類載入器)開始載入指定的類,在載入類時,每個類載入器會將載入任務上交給其父,如果其父找不到,再由自己去載入, Bootstrap Loader (啟動類載入器)是最頂級的類載

成為Java GC專家(1)深入淺出Java垃圾回收機制

  對於Java開發人員來說,瞭解垃圾回收機制(GC)有哪些好處呢?首先可以滿足作為一名軟體工程師的求知慾,其次,深入瞭解GC如何工作可以幫你寫出更好的Java應用。   這僅僅代表我個人的意見,但我堅信一個精通GC的人往往是一個好的Java開發者。如果你對GC的處理過程感

JAVA垃圾回收機制怎麼工作

java垃圾回收時間不確定 在棧中的對於物件的引用失效 堆中存放的資料不會被回收 只有等待GC回收同時GC回收的是堆區和方法區的記憶體。堆記憶體用來存放由new建立的物件和陣列。 棧記憶體(Stack):每個執行緒私有的。 堆記憶體(Heap):所有執行緒公用的。java  

成為Java GC專家(2)如何監控Java垃圾回收機制

  本文是成為Java GC專家系列文章的第二篇。在第一篇《深入淺出Java垃圾回收機制》中我們學習了不同GC演算法的執行過程,GC是如何工作的,什麼是新生代和老年代,你應該瞭解的JDK7中的5種GC型別,以及這5種類型對於應用效能的影響。   在本文中,我將解釋JVM到底