前言:內存泄漏寫任何語言都必須得注意的問題,我司技術老大平常吐槽:之前作遊戲的改內存泄漏的bug,如今寫前端仍是這些問題。javascript
內存泄漏能夠定義爲程序再也不使用或不須要的一塊內存,可是因爲某種緣由沒有被釋放仍然被沒必要要的佔有。在代碼中建立對象和變量會佔用內存,可是javaScript是有本身的內存回收機制,能夠肯定那些變量再也不須要,並將其清除。可是當你的代碼存在邏輯缺陷的時候,你覺得你已經不須要,可是程序中還存在着引用,致使程序運行完後並無合適的回收所佔用的空間,致使內存不斷的佔用,運行的時間越長佔用的就越多,隨之出現的是,性能不佳,高延遲,頻繁崩潰。
在深刻了解內存泄漏以前,咱們須要知道一下幾點:前端
無論什麼程序語言,內存生命週期基本是一致的:文章封面圖java
全部語言第二部分都是明確的。第一和第三部分在底層語言中是明確的,但在像JavaScript這些高級語言中,大部分都是隱含的。node
不一樣的語言經過不一樣的方式來處理其內存。算法
垃圾回收機制經過按期的檢查哪些先前分配的內存「仍然被須要」(×)
垃圾回收機制經過按期的檢查哪些先前分配的內存「程序的其餘部分仍然能夠訪問到的內存」(√)。數組
嗯? 一臉懵逼。。。。瀏覽器
這是理解垃圾回收的關鍵,只有開發者知道這一塊內存是否在將來使用「被須要」,可是能夠經過算法來肯定沒法訪問的內存並將其標記返回操做系統。session
這是最初級的垃圾收集算法,若是沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。(MDN上面的例子)閉包
var o = { a: { b:2 } }; // 兩個對象被建立,一個做爲另外一個的屬性被引用, 另外一個被分配給變量o // 很顯然,沒有一個能夠被垃圾收集 var o2 = o; // o2變量是第二個對「這個對象」的引用 o = 1; // 如今,「這個對象」的原始引用o被o2替換了 var oa = o2.a; // 引用「這個對象」的a屬性 // 如今,「這個對象」有兩個引用了,一個是o2,一個是oa o2 = "yo"; // 最初的對象如今已是零引用了 // 他能夠被垃圾回收了 // 然而它的屬性a的對象還在被oa引用,因此還不能回收 oa = null; // a屬性的那個對象如今也是零引用了 // 它能夠被垃圾回收了
缺陷:在循環的狀況下,引用計數算法存在很大的侷限性。併發
function foo(){ var obj1 = {}; var obj2 = {}; obj1.x = obj2 ; // obj1引用obj2 obj2.x = obj1 ; // obj2引用obj1 return true ; } foo();
算法由如下幾步組成:
無圖言( )
循環引用的問題迎刃而解.雖然這個算法在不停的改進,js垃圾回收的(生成/增量/併發垃圾回收)這些改進的本質上仍是相同的: 可達內存被標記,其他的被看成垃圾回收。
缺點: 算法運行時程序執行被暫停。
### 垃圾回收機制不可預測
雖然垃圾回收機制很好很方便,可是得本身權衡.緣由是咱們沒法肯定什麼時候會執行收集.只有開發人員才能明確是否能夠將一塊內存返回給操做系統。不須要的引用是指開發者明知內存引用再也不須要,而咱們在寫程序的時候卻因爲某些緣由,它仍被留在激活的 root 樹中。
### 如何避免
垃圾收集語言泄漏的主要緣由是不須要的引用。要了解不須要的引用是什麼,首先咱們須要瞭解垃圾收集器如何肯定是否能夠訪問一塊內存。
所以,要了解哪些是JavaScript中最多見的泄漏,咱們須要知道引用一般被遺忘的方式。
### 常見的四種內存泄漏
#### 1. 全局變量
在非嚴格模式下當引用未聲明的變量時,會在全局對象中建立一個新變量。在瀏覽器中,全局對象將是window,這意味着
function foo(arg){ bar =「some text」; // bar將泄漏到全局. }
爲何不能泄漏到全局呢,咱們平時都會定義全局變量呢!!!
緣由 :全局變量是根據定義沒法被垃圾回收機制收集.須要特別注意用於臨時存儲和處理大量信息的全局變量。若是必須使用全局變量來存儲數據,請確保將其指定爲null或在完成後從新分配它。
解決辦法 : 嚴格模式
var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { node.innerHTML = JSON.stringify(someResource)); // 定時器也沒有清除 } // node、someResource 存儲了大量數據 沒法回收 }, 1000);
緣由:與節點或數據關聯的計時器再也不須要,node 對象能夠刪除,整個回調函數也不須要了。但是,計時器回調函數仍然沒被回收(計時器中止纔會被回收)。同時,someResource 若是存儲了大量的數據,也是沒法被回收的。
解決方法: 在定時器完成工做的時候,手動清除定時器
var refA = document.getElementById('refA'); document.body.removeChild(refA); // dom刪除了 console.log(refA, "refA"); // 可是還存在引用 能console出整個div 沒有被回收
緣由: 保留了DOM節點的引用,致使GC沒有回收
解決辦法:refA = null;
注意: 此外還要考慮 DOM 樹內部或子節點的引用問題。假如你的 JavaScript 代碼中保存了表格某一個 <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 迫使它保留在內存中(防止被回收)。當這段代碼反覆運行,就會看到內存佔用不斷上升,垃圾回收器(GC)並沒有法下降內存佔用。本質上,閉包的鏈表已經建立,每個閉包做用域攜帶一個指向大數組的間接的引用,形成嚴重的內存泄漏。
解決: 去除unuserd函數或者在replaceThing函數最後一行加上 originlThing = null.
參考:
4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
How JavaScript works: memory management + how to handle 4 common memory leaks