JSfor循環刪除指定節點「js刪除節點的方法」

內存管理

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 內失去對用戶的響應,造成假死現象。如果有動畫效果的話,動畫的展現也將顯著受到影響。

V8 垃圾回收策略

  • 採用分代回收的思想
  • 內存分為新生代、老生代
  • 針對新、老生代採用不同演算法來提升垃圾回收的效率

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

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

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

新生代對象回收實現

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

晉陞

將新生代對象移到老生代

晉陞條件

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

老生代對象回收實現

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

細節對比

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

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

V8 的優化

增量標記

由於全停頓會造成了瀏覽器一段時間無響應,所以 V8 使用了一種增量標記的方式,將完整的標記拆分成很多部分,每做完一部分就停下來,讓 JS 的應用邏輯執行一會,這樣垃圾回收與應用邏輯交替完成。經過增量標記的改進後,垃圾回收的最大停頓時間可以減少到原來的 1/6 左右

JavaScript的內存和內存管理

惰性清理

由於標記完成後,所有的對象都已經被標記,不是死對象就是活對象,堆上多少空間格局已經確定。我們可以不必著急釋放那些死對象所佔用的空間,而延遲清理過程的執行。垃圾回收器可以根據需要逐一清理死對象所佔用的內存空間

其他

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 結束錄製之後,開啟內存勾選,拖動截圖到指定時間段查看發生內存問題時候到頁面展示,並定位問題。同時可以查看對應出現紅點到執行腳本,定位問題代碼。

JavaScript的內存和內存管理

利用瀏覽器內存模塊,查找分離 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;
}
JavaScript的內存和內存管理

如何確定頻繁對垃圾回收

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

代碼優化

jsPerf(JavaScript 性能測試)

基於 Benchmark.js

慎用全局變數

  • 全局變數定義在全局執行的上下文,是所有作用域鏈的頂端
  • 全局執行上下文一直存在於上下文執行棧,直到程序退出
  • 如果某個局部作用域出現了同名變數則會屏蔽或者污染全局作用域
  • 全局變數的執行速度,訪問速度要低於局部變數,因此對於一些需要經常訪問的全局變數可以在局部作用域中進行緩存
JavaScript的內存和內存管理

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

避免全局查找

JavaScript的內存和內存管理

上圖可以看出,test2 的性能要比 test1 的性能要好,從而得知,緩存全局變數後使用可以提升性能

通過原型對象添加附加方法提高性能

JavaScript的內存和內存管理

上圖可以看出,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 不需屬性的訪問方法,所有屬性都是外部可見的
  • 使用屬性訪問方法只會增加一層重定義,沒有訪問的控制力
JavaScript的內存和內存管理

上圖可以看出,test2 的性能要比 test1 的性能要好不少,從而得知,直接訪問屬性,會比通過方法訪問屬性速度來的快。

遍歷速度

JavaScript的內存和內存管理

上圖可以看出,loop 遍歷速度 forEach > 優化 for > for of > for > for in

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/268746.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-16 13:11
下一篇 2024-12-16 13:11

相關推薦

發表回復

登錄後才能評論