JavaScript的內存和內存管理

這是我參與8月更文挑戰的第6天,活動詳情查看:8月更文挑戰web

內存管理

V8 內存限制

限制大小算法

64 位爲 1.4GB,32 位爲 0.7GB瀏覽器

限制緣由緩存

V8 之因此限制了內存的大小,表面上的緣由是 V8 最初是做爲瀏覽器的 JavaScript 引擎而設計,不太可能遇到大量內存的場景,而深層次的緣由則是因爲 V8 的垃圾回收機制的限制。因爲 V8 須要保證 JavaScript 應用邏輯與垃圾回收器所看到的不同,V8 在執行垃圾回收時會阻塞 JavaScript 應用邏輯,直到垃圾回收結束再從新執行 JavaScript 應用邏輯,這種行爲被稱爲「全停頓」(stop-the-world)。若 V8 的堆內存爲 1.5GB,V8 作一次小的垃圾回收須要 50ms 以上,作一次非增量式的垃圾回收甚至要 1 秒以上。這樣瀏覽器將在 1s 內失去對用戶的響應,形成假死現象。若是有動畫效果的話,動畫的展示也將顯著受到影響。markdown

V8 垃圾回收策略

  • 採用分代回收的思想
  • 內存分爲新生代、老生代
  • 針對新、老生代採用不一樣算法來提高垃圾回收的效率

新生代的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象。閉包

V8 新生代、老生代內存大小

V8 引擎的新生代內存大小 32MB(64 位)、16MB(32 位),老生代內存大小爲 1400MB(64 位)、700MB( 32 位)。app

新生代對象回收實現

  • 回收過程採用複製算法+標記整理
  • 新生代內存區被等分爲兩個空間
  • 使用空間爲 From,空閒空間爲 To
  • 標記整理後將活動對象拷貝至 To
  • From 和 To 交換空間完成釋放

image.png

晉升

將新生代對象移到老生代dom

晉升條件jsp

  • 一輪 GC 還存活的新生代須要晉升
  • 對象從 From 空間複製到 To 空間時,若是 To 空間已經被使用了超過 25%,那麼這個對象直接被複制到老生代

老生代對象回收實現

  • 主要採起標記清除、標記整理、增量標記算法
  • 首先使用標記清除完成垃圾空間的回收
  • 採用標記整理進行空間優化
  • 採用增量標記進行效率優化

細節對比

新生代區域,採用複製算法, 所以其每時每刻內部都有空閒空間的存在(爲了完成 From 到 To 的對象複製),可是新生代區域空間較小(32M)且被一分爲二,因此這種空間上的浪費也是比較微不足道的。oop

老生代因其空間較大(1.4G),若是一樣採用一分爲二的作法則對空間大小是比較浪費,且老生代空間較大,存放對對象也較多,若是進行復制算法,則其消耗對時間也會更大。也就是是否使用複製算法來進行垃圾回收,是一個時間 T 關於內存大小的關係,當內存大小較小時,使用複製算法消耗的時間是比較短的,而當內存較大時,採用複製算法對時間對消耗也就更大。

V8 的優化

增量標記

因爲全停頓會形成了瀏覽器一段時間無響應,因此 V8 使用了一種增量標記的方式,將完整的標記拆分紅不少部分,每作完一部分就停下來,讓 JS 的應用邏輯執行一會,這樣垃圾回收與應用邏輯交替完成。通過增量標記的改進後,垃圾回收的最大停頓時間能夠減小到原來的 1/6 左右

image.png

惰性清理

因爲標記完成後,全部的對象都已經被標記,不是死對象就是活對象,堆上多少空間格局已經肯定。咱們能夠沒必要着急釋放那些死對象所佔用的空間,而延遲清理過程的執行。垃圾回收器能夠根據須要逐一清理死對象所佔用的內存空間

其餘

V8 後續還引入了增量式整理(incremental compaction),以及並行標記和並行清理,經過並行利用多核 CPU 來提高垃圾回收的性能

監控內存

內存問題的外在表現

  • 頁面出現延遲加載或常常性暫停: 可能存在頻繁當 GC 操做,存在一些代碼瞬間吃滿了內存。
  • 頁面出現持續性的糟糕性能: 程序爲了達到最優的運行速度,向內存申請了一片較大的內存空間,但空間大小超過了設備所能提供的大小。
  • 頁面使用隨着時間延長愈來愈卡: 可能存在內存泄漏。

界定內存問題的標準

  • 內存泄漏:內存使用持續升高
  • 內存膨脹:在多數設備上都存在性能問題
  • 頻繁垃圾回收:經過內存變化時序圖進行分析

監控內存方式

任務管理器

這裏以 Google 瀏覽器爲例,使用 Shift + Esc 喚起 Google 瀏覽器自帶的任務管理器

  • Memory(內存) 列表示原生內存。DOM 節點存儲在原生內存中。 若是此值正在增大,則說明正在建立 DOM 節點。
  • JavaScript Memory(JavaScript 內存) 列表示 JS 堆。此列包含兩個值。 您感興趣的值是實時數字(括號中的數字)。 實時數字表示您的頁面上的可到達對象正在使用的內存量。 若是此數字在增大,要麼是正在建立新對象,要麼是現有對象正在增加。

模擬內存泄漏

在任務管理器裏能夠看到 JavaScript 內存持續上升

document.body.innerHTML = `<button id="add">add</button>`;
document.getElementById('add').addEventListener('click', function (e) {
  simulateMemoryLeak();
});
let result = [];
function simulateMemoryLeak() {
  setInterval(function () {
    result.push(new Array(1000000).join('x'));
    document.body.innerHTML = result;
  }, 100);
}
複製代碼

Timeline 記錄內存

這裏以 Google 瀏覽器爲例,使用 F12 開啓調式,選擇 Performance,點擊 record(錄製),進行頁面操做,點擊 stop 結束錄製以後,開啓內存勾選,拖動截圖到指定時間段查看發生內存問題時候到頁面展現,並定位問題。同時能夠查看對應出現紅點到執行腳本,定位問題代碼。

image.png

利用瀏覽器內存模塊,查找分離 dom

這裏以 Google 瀏覽器爲例,在頁面上進行相關操做後,使用 F12 開啓調式,選擇 Memory,點擊 Take snapshot(拍照),在快照中查找 Detached HTMLElement,回到代碼中查找對應的分離 dom 存在的代碼,在相關操做代碼以後,對分離 dom 進行釋放,防止內存泄漏。

只有頁面的 DOM 樹或 JavaScript 代碼再也不引用 DOM 節點時,DOM 節點纔會被做爲垃圾進行回收。 若是某個節點已從 DOM 樹移除,但某些 JavaScript 仍然引用它,咱們稱此節點爲「已分離」。已分離的 DOM 節點是內存泄漏的常見緣由。

模擬已分離 DOM 節點

document.body.innerHTML = `<button id="add">add</button>`;
document.getElementById('add').addEventListener('click', function (e) {
  create();
});
let detachedTree;
function create() {
  let ul = document.createElement('ul');
  for (let i = 0; i < 10; i++) {
    let li = document.createElement('li');
    ul.appendChild(li);
  }
  detachedTree = ul;
}
複製代碼

image.png

如何肯定頻繁對垃圾回收

  • GC 工做時,程序是暫停的,頻繁/過長的 GC 會致使程序假死,用戶會感知到卡頓。
  • 查看 Timeline 中是否存在內存走向在短期內頻繁上升降低的區域。瀏覽器任務管理器是否頻繁的增長減小。

代碼優化

jsPerf(JavaScript 性能測試)

基於 Benchmark.js

慎用全局變量

  • 全局變量定義在全局執行的上下文,是全部做用域鏈的頂端
  • 全局執行上下文一直存在於上下文執行棧,直到程序退出
  • 若是某個局部做用域出現了同名變量則會屏蔽或者污染全局做用域
  • 全局變量的執行速度,訪問速度要低於局部變量,所以對於一些須要常常訪問的全局變量能夠在局部做用域中進行緩存

image.png

上圖能夠看出,test2 的性能要比 test1 的性能要好,從而得知,全局變量的執行速度,訪問速度要低於局部變量

避免全局查找

image.png 上圖能夠看出,test2 的性能要比 test1 的性能要好,從而得知,緩存全局變量後使用能夠提高性能

經過原型對象添加附加方法提升性能

image.png 上圖能夠看出,test2 的性能要比 test1 的性能要好,從而得知,經過原型對象添加方法與直接在對象上添加成員方法相比,原型對象上的屬性訪問速度較快。

避開閉包陷阱

閉包特色

  • 外部具備指向內部的引用
  • 在「外」部做用域訪問「內」部做用域的數據
function foo() {
  let name = 'heath';
  function fn() {
    console.log(name);
  }
  return fn;
}
let a = foo();
a();
複製代碼

閉包使用不當很容易出現內存泄漏

function f5() {
  // el 引用了全局變量document,假設btn節點被刪除後,由於這裏被引用着,因此這裏不會被垃圾回收,致使內存泄漏
  let el = document.getElementById('btn');
  el.onclick = function (e) {
    console.log(e.id);
  };
}
f5();

function f6() {
  // el 引用了全局變量document,假設btn節點被刪除後,由於這裏被引用着,因此這裏不會被垃圾回收,致使內存泄漏
  let el = document.getElementById('btn');
  el.onclick = function (e) {
    console.log(e.id);
  };
  el = null; // 咱們這裏手動將el內存釋放,從而當btn節點被刪除後,能夠被垃圾回收
}
f6();
複製代碼

避免屬性訪問方法使用

JavaScript 中的面向對象

  • JS 不需屬性的訪問方法,全部屬性都是外部可見的
  • 使用屬性訪問方法只會增長一層重定義,沒有訪問的控制力

image.png 上圖能夠看出,test2 的性能要比 test1 的性能要好很多,從而得知,直接訪問屬性,會比經過方法訪問屬性速度來的快。

遍歷速度

image.png 上圖能夠看出,loop 遍歷速度 forEach > 優化 for > for of > for > for in

dom 節點操做

upload-images.jianshu.io/upload_imag… 上圖能夠看出,節點克隆(cloneNode)生成節點速度要快於建立節點。

採用字面量替換 New 操做

image.png 上圖能夠看出,字面量聲明的數據生成速度要快於單獨屬性賦值行爲生成的數據。

最後最後:

公衆號:小何成長,佛系更文,都是本身曾經踩過的坑或者是學到的東西

有興趣的小夥伴歡迎關注我哦,我是:何小玍。 你們一塊兒進步鴨

相關文章
相關標籤/搜索