一塊兒來看Javascript的垃圾回收機制

JS的垃圾回收機制

JS會在建立變量時自動分配內存,在不使用的時候會自動週期性的釋放內存,釋放的過程就叫 "垃圾回收"。這個機制有好的一面,固然也也有很差的一面。一方面自動分配內存減輕了開發者的負擔,開發者不用過多的去關注內存使用,可是另外一方面,正是由於由於是自動回收,因此若是不清楚回收的機制,會很容易形成混亂,而混亂就很容易形成"內存泄漏".因爲是自動回收,因此就存在一個 "內存是否須要被回收的" 的問題,可是這個問題的斷定在程序中意味着沒法經過某個算法去準確完整的解決,後面探討的回收機制只能有限的去解決通常的問題。javascript

回收算法

垃圾回收對是否須要回收的問題主要依賴於對變量的斷定是否可訪問,由此衍生出兩種主要的回收算法:html

  • 標記清理
  • 引用計數

標記清理

標記清理是js最經常使用的回收策略,2012年後全部瀏覽器都使用了這種策略,此後的對回收策略的改進也是基於這個策略的改進。其策略是:java

  1. 變量進入上下文,也可理解爲做用域,會加上標記,證實其存在於該上下文;
  2. 將全部在上下文中的變量以及上下文中被訪問引用的變量標記去掉,代表這些變量活躍有用;
  3. 在此以後再被加上標記的變量標記爲準備刪除的變量,由於上下文中的變量已經沒法訪問它們;
  4. 執行內存清理,銷燬帶標記的全部非活躍值並回收以前被佔用的內存;

過程

侷限
  • 因爲是從根對象(全局對象)開始查找,對於那些沒法從根對象查詢到的對象都將被清除
  • 回收後會造成內存碎片,影響後面申請大的連續內存空間

引用計數

引用計數策略相對而言不經常使用,由於弊端較多。其思路是對每一個值記錄它被引用的次數,經過最後對次數的判斷(引用數爲0)來決定是否保留,具體的規則有git

  • 聲明一個變量,賦予它一個引用值時,計數+1;
  • 同一個值被賦予另一個變量時,引用+1;
  • 保存對該值引用的變量被其餘值覆蓋,引用-1;
  • 引用爲0,回收內存;
侷限

最重要的問題就是,循環引用 的問題github

function refProblem () {
    let a = new Object();
    let b = new Object();
    a.c = b;
    b.c = a;  //互相引用
}

根據以前提到的規則,兩個都互相引用了,引用計數不爲0,因此兩個變量都沒法回收。若是頻繁的調用改函數,則會形成很嚴重的內存泄漏。算法

Nodejs V8回收機制

V8的回收機制基於 分代回收機制 ,將內存分爲新生代(young generation)和老生代(tenured generation),新生代爲存活時間較短的對象,老生代爲存活時間較長或者常駐內存的變量。瀏覽器

新生代老生代

V8堆的構成

V8將堆分紅了幾個不一樣的區域閉包

堆

  • 新生代(New Space/Young Generation): 大多數新生對象被分配到這,分爲兩塊空間,總體佔據小塊空間,垃圾回收的頻率較高,採用的回收算法爲 Scavenge 算法
  • 老生代(Old Space/Old Generation):大多數在新生區存活一段時間後的對象會轉移至此,採用的回收算法爲 標記清除 & 整理(Mark-Sweep & Mark-Compact,Major GC) 算法,內部再細分爲兩個空間ide

    • 指針空間(Old pointer space): 存儲的對象含有指向其餘對象的指針
    • 數據空間(Old data space):存儲的對象僅包含數據,無指向其餘對象的指針
  • 大對象空間(Large Object Space):存放超過其餘空間(Space)限制的大對象,垃圾回收器從不移動此空間中的對象
  • 代碼空間(Code Space): 代碼對象,用於存放代碼段,是惟一擁有執行權限的內存空間,須要注意的是若是代碼對象太大而被移入大對象空間,這個代碼對象在大對象空間內也是擁有執行權限的,但不能所以說大對象空間也有執行權限
  • Cell空間、屬性空間、Map空間 (Cell ,Property,Map Space): 這些區域存放Cell、屬性Cell和Map,每一個空間由於都是存放相同大小的元素,所以內存結構很簡單。

Scavenge 算法

Scavenge 算法是新生代空間中的主要算法,該算法由 C.J. Cheney 在 1970 年在論文 A nonrecursive list compacting algorithm 提出。
Scavenge 主要採用了 Cheney算法,Cheney算法新生代空間的堆內存分爲2塊一樣大小的空間,稱爲 Semi space,處於使用狀態的成爲 From空間 ,閒置的稱爲 To 空間。垃圾回收過程以下:函數

  • 檢查From空間,若是From空間被分配滿了,則執行Scavenge算法進行垃圾回收
  • 若是未分配滿,則檢查From空間的是否有存活對象,若是無存活對象,則直接釋放未存活對象的空間
  • 若是存活,將檢查對象是否符合晉升條件,若是符合晉升條件,則移入老生代空間,不然將對象複製進To空間
  • 完成複製後將From和To空間角色互換,而後再從第一步開始執行
晉升條件
  1. 經歷過一次Scavenge 算法篩選;
  2. To空間內存使用超過25%;

晉升

標記清除 & 整理(Mark-Sweep & Mark-Compact,Major GC)算法

以前說過,標記清除策略會產生內存碎片,從而影響內存的使用,這裏 標記整理算法(Mark-Compact)的出現就能很好的解決這個問題。標記整理算法是在 標記清除(Mark-Sweep )的基礎上演變而來的,整理算法會將活躍的對象往邊界移動,完成移動後,再清除不活躍的對象。

整理過程

因爲須要移動移動對象,因此在處理速度上,會慢於Mark-Sweep。

全停頓(Stop The World )

爲了不應用邏輯與垃圾回收器看到的邏輯不同,垃圾回收器在執行回收時會中止應用邏輯,執行完回收任務後,再繼續執行應用邏輯。這種行爲就是 全停頓,停頓的時間取決與不一樣引擎執行一次垃圾回收的時間。這種停頓對新生代空間的影響較小,但對老生代空間可能會形成停頓的現象。

增量標記(Incremental Marking)

爲了解決全停頓的現象,2011年V8推出了增量標記。V8將標記過程分爲一個個的子標記過程,同時讓垃圾回收標記和JS應用邏輯交替進行,直至標記完成。

增量標記

內存泄漏

內存泄漏的問題難以察覺,在函數被調用不少次的狀況下,內存泄漏多是個大問題。常見的內存泄漏主要有下面幾個場景。

意外聲明全局變量
function hello (){
    name = 'tom'
}
hello();

未聲明的對象會被綁定在全局對象上,就算不被使用了,也不會被回收,因此寫代碼的時候,必定要記得聲明變量。

定時器
let name = 'Tom';
setInterval(() => {
  console.log(name);
}, 100);

定時器的回調經過閉包引用了外部變量,若是定時器不清除,name會一直佔用着內存,因此用定時器的時候最好明白本身須要哪些變量,檢查定時器內部的變量,另外若是不用定時器了,記得及時清除定時器。

閉包
let out = function() {
  let name = 'Tom';
  return function () {
    console.log(name);
  }
}

因爲閉包會常駐內存,在這個例子中,若是out一直存在,name就一直不會被清理,若是name值很大的時候,就會形成比較嚴重的內存泄漏。因此必定要慎重使用閉包。

事件監聽
mounted() {
window.addEventListener("resize",  () => {
    //do something
});
}

在頁面初始化時綁定了事件監聽,可是在頁面離開的時候未清除事監聽,就會致使內存泄漏。

最後

文章爲參考資料總結的筆記文章,我最近在重學js,會將複習總結的文章記錄在Github,戳這, 有想一塊兒複習的小夥伴可一塊兒參與複習總結!

參考資料
  1. 有意思的 Node.js 內存泄漏問題
  2. js 垃圾回收機制
  3. A tour of V8: Garbage Collection
  4. JS探索-GC垃圾回收
  5. JavaScript內存管理
  6. JavaScript高級程序設計(第4版)
相關文章
相關標籤/搜索