1. 程式人生 > 其它 >ie圖片壓縮後如何釋放記憶體_JavaScript系列之記憶體洩漏

ie圖片壓縮後如何釋放記憶體_JavaScript系列之記憶體洩漏

技術標籤:ie圖片壓縮後如何釋放記憶體

3afe4730e4a1afa86a0b1ff97f73f4f3.png

在程式執行過程中不再用到的記憶體,沒有及時釋放,會出現記憶體洩漏(memory leak),會造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。

而記憶體洩漏是每個開發人員最終必須面對的問題。 即使使用記憶體管理語言,比如C語言有著malloc() 和 free() 這種低階記憶體管理語言也有可能出現洩露記憶體的情況。

這很麻煩,所以為了減輕程式設計中的負擔,大多數語言提供了自動記憶體管理,這被稱為"垃圾回收機制"(garbage collector)。

垃圾回收機制

現在各大瀏覽器通常採用的垃圾回收有兩種方法:標記清除(mark and sweep)

引用計數(reference counting)

1、標記清除

這是javascript中最常用的垃圾回收方式。

工作原理:當變數進入執行環境時,將這個變數標記為“進入環境”。當變數離開環境時,則將其標記為“離開環境”。標記“離開環境”的就回收記憶體。

工作流程:

  1. 垃圾回收器,在執行的時候會給儲存在記憶體中的所有變數都加上標記。
  2. 去掉環境中的變數以及被環境中的變數引用的變數的標記。
  3. 之後再被加上標記的變數將被視為準備刪除的變數。
  4. 垃圾回收器完成記憶體清除工作,銷燬那些帶標記的值並回收他們所佔用的記憶體空間。

2、引用計數

工作原理:跟蹤記錄每個值被引用的次數。

工作流程:

  1. 將一個引用型別的值賦值給這個聲明瞭的變數,這個引用型別值的引用次數就是1。
  2. 同一個值又被賦值給另一個變數,這個引用型別值的引用次數加1。
  3. 當包含這個引用型別值的變數又被賦值成另一個值了,那麼這個引用型別值的引用次數減1
  4. 當引用次數變成0時,就表示這個值不再用到了。
  5. 當垃圾收集器下一次執行時,它就會釋放引用次數是0的值所佔的記憶體。

但如果一個值不再需要了,引用數卻不為0,垃圾回收機制無法釋放這塊記憶體,會導致記憶體洩漏。

var arr = [1, 2, 3];
console.log('hello miqilin');

上面程式碼中,陣列[1, 2, 3]會佔用記憶體,賦值給了變數arr,因此引用次數為1。儘管後面的一段程式碼沒有用到arr,它還是會持續佔用記憶體。

如果增加一行程式碼,解除arr對[1, 2, 3]引用,這塊記憶體就可以被垃圾回收機制釋放了。

var arr = [1, 2, 3];
console.log('hello miqilin');
arr = null;

上面程式碼中,arr重置為null,就解除了對[1, 2, 3]的引用,引用次數變成了0,記憶體就可以釋放出來了。

因此,並不是說有了垃圾回收機制,程式設計師就無事一身輕了。你還是需要關注記憶體佔用:那些很佔空間的值,一旦不再用到,你必須檢查是否還存在對它們的引用。如果是的話,就必須手動解除引用。

接下來,我將介紹四種常見的JavaScript 記憶體洩漏及如何避免。目前水平有限,借鑑了國外大牛的文章瞭解這幾種記憶體洩漏,原文連結:https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec

四種常見的 JavaScript 記憶體洩漏

1.意外的全域性變數

未定義的變數會在全域性物件建立一個新變數,對於在瀏覽器的情況下,全域性物件是window。 看以下程式碼:

function foo(arg) {
     bar = "this is a hidden global variable"; 
}

函式foo內部使用var宣告,實際上JS會把bar掛載在全域性物件上,意外建立一個全域性變數。等同於:

function foo(arg) {
     window.bar = "this is an explicit global variable"; 
}

在上述情況下, 洩漏一個簡單的字串不會造成太大的傷害,但它肯定會更糟。

另一種可以建立偶然全域性變數的情況是this:

function foo() {
     this.variable = "potential accidental global"; 
}  
// Foo called on its own, this points to the global object (window)
// rather than being undefined. 
foo();

解決方法

在 JavaScript 檔案頭部加上 'use strict',使用嚴格模式避免意外的全域性變數,此時上例中的this指向undefined。如果必須使用全域性變數儲存大量資料時,確保用完以後把它設定為 null 或者重新定義。

2.被遺忘的計時器或回撥函式

在JavaScript中使用setInterval非常常見。

var someResource = getData(); 
setInterval(function() {
     var node = document.getElementById('Node');     
     if(node) {
         // Do stuff with node and someResource.
         node.innerHTML = JSON.stringify(someResource));
     } }, 1000);

上面的程式碼表明,在節點node或者資料不再需要時,定時器依舊指向這些資料。所以哪怕當node節點被移除後,interval 仍舊存活並且垃圾回收器沒辦法回收,它的依賴也沒辦法被回收,除非終止定時器。

var element = document.getElementById('button');  

function onClick(event) {
     element.innerHtml = 'text'; 
}  

element.addEventListener('click', onClick); // Do stuff 
element.removeEventListener('click', onClick); 
element.parentNode.removeChild(element); 

// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.

對於上面觀察者的例子,一旦它們不再需要(或者關聯的物件變成不可達),明確地移除它們非常重要。其中IE 6 是無法處理迴圈引用的。因為老版本的 IE 是無法檢測 DOM 節點與 JavaScript 程式碼之間的迴圈引用,會導致記憶體洩漏。

但是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收演算法(標記清除),已經可以正確檢測和處理迴圈引用了。即回收節點記憶體時,不必非要呼叫removeEventListener了。

諸如jQuery之類的框架和庫在處理節點之前會刪除偵聽器(當使用它們的特定API時)。 這由庫內部處理,並確保不會產生任何洩漏,即使在有問題的瀏覽器(如舊版Internet Explorer)下執行也是如此。

3.閉包

JavaScript 開發的一個關鍵是閉包:這是一個內部函式,它可以訪問外部(封閉)函式的變數。由於 JavaScript 執行時的實現細節,用下邊這種方式可能會造成記憶體洩漏:

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

每次呼叫replaceThingtheThing得到一個包含一個大陣列和一個新閉包(someMethod)的新物件。同時,變數unused是一個引用originalThing的閉包(先前的replaceThing又呼叫了theThing)。someMethod可以通過theThing使用,someMethodunused分享閉包作用域,儘管unused從未使用,它引用的originalThing迫使它保留在記憶體中(防止被回收)。需要記住的是一旦一個閉包作用域被同一個父作用域的閉包所建立,那麼這個作用域是共享的。

所有這些都可能導致嚴重的記憶體洩漏。當上面的程式碼片段一次又一次地執行時,你可以看到記憶體使用量的急劇增加。當垃圾收集器執行時,也不會減少。一個連結列表閉包被建立(在這種情況下 theThing 變數是根源),每一個閉包作用域對打陣列進行間接引用。

解決方法

replaceThing 的最後新增 originalThing = null 。將所有聯絡都切斷。

4.脫離 DOM 的引用

如果把DOM 存成字典(JSON 鍵值對)或者陣列,此時,同樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另一個在字典中。如果在將來某個時候您決定刪除這些行,則需要使兩個引用都無法訪問,都清除掉。

var elements = {
     button: document.getElementById('button'),
     image: document.getElementById('image'),
     text: document.getElementById('text') 
}; 

function doStuff() {
     image.src = 'http://some.url/image';
     button.click();
     console.log(text.innerHTML);
     // Much more logic
} 
 
function removeButton() {
     // The button is a direct child of body.
     document.body.removeChild(document.getElementById('button'));

    // At this point, we still have a reference to #button in the global
    // elements dictionary. In other words, the button element is still in
    // memory and cannot be collected by the GC. 
}

如果程式碼中儲存了表格某一個<td>的引用。將來決定刪除整個表格的時候,直覺認為 GC 會回收除了已儲存的<td>以外的其它節點。實際情況並非如此:此<td>是表格的子節點,子元素與父元素是引用關係。由於程式碼保留了<td>的引用,導致整個表格仍待在記憶體中。所以儲存 DOM 元素引用的時候,要小心謹慎。

避免記憶體洩漏

在區域性作用域中,等函式執行完畢,變數就沒有存在的必要了,js垃圾回收機制很快做出判斷並且回收,但是全域性變數什麼時候需要自動釋放記憶體空間則很難判斷,因此在我們的開發中,需要儘量避免使用全域性變數。

我們在使用閉包的時候,就會造成嚴重的記憶體洩漏,因為閉包的原因,區域性變數會一直儲存在記憶體中,所以在使用閉包的時候,要多加小心。

Resources

  • http://www-bcf.usc.edu/~dkempe/CS104/08-29.pdf
  • https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156
  • http://www.nodesimplified.com/2017/08/javascript-memory-management-and.html

如果有別的關於記憶體洩漏好的資源,可以分享給我嘛謝謝了~

本人Github連結如下,歡迎各位Star

https://github.com/miqilin21/miqilin21.github.io