Chrome 瀏覽器垃圾回收機制與記憶體泄漏分析

来源:https://www.cnblogs.com/LuckyWinty/archive/2019/10/25/11739573.html
-Advertisement-
Play Games

Chorme 瀏覽器中的垃圾回收和記憶體泄漏 垃圾回收 通常情況下,垃圾數據回收分為 和`自動回收`兩種策略。 手動回收策略,何時分配記憶體、何時銷毀記憶體都是由代碼控制的。 自動回收策略,產生的垃圾數據是由垃圾回收器來釋放的,並不需要手動通過代碼來釋放。 JavaScript 中調用棧中的數據回收 Ja ...


Chorme 瀏覽器中的垃圾回收和記憶體泄漏

垃圾回收

通常情況下,垃圾數據回收分為手動回收自動回收兩種策略。

手動回收策略,何時分配記憶體、何時銷毀記憶體都是由代碼控制的。
自動回收策略,產生的垃圾數據是由垃圾回收器來釋放的,並不需要手動通過代碼來釋放。

JavaScript 中調用棧中的數據回收

JavaScript 引擎會通過向下移動 ESP(記錄當前執行狀態的指針) 來銷毀該函數保存在棧中的執行上下文。

JavaScript 堆中的數據回收

在 V8 中會把堆分為新生代老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放的生存時間久的對象。

新生區通常只支持 1~8M 的容量,而老生區支持的容量就大很多了。對於這兩塊區域,V8 分別使用兩個不同的垃圾回收器,以便更高效地實施垃圾回收。

  • 副垃圾回收器,主要負責新生代的垃圾回收。
  • 主垃圾回收器,主要負責老生代的垃圾回收。

不論什麼類型的垃圾回收器,它們都有一套共同的執行流程。

  1. 第一步是標記空間中活動對象和非活動對象。所謂活動對象就是還在使用的對象,非活動對象就是可以進行垃圾回收的對象。
  2. 第二步是回收非活動對象所占據的記憶體。其實就是在所有的標記完成之後,統一清理記憶體中所有被標記為可回收的對象。
  3. 第三步是做記憶體整理。一般來說,頻繁回收對象後,記憶體中就會存在大量不連續空間,我們把這些不連續的記憶體空間稱為記憶體碎片,。當記憶體中出現了大量的記憶體碎片之後,如果需要分配較大連續記憶體的時候,就有可能出現記憶體不足的情況。所以最後一步需要整理這些記憶體碎片。(這步其實是可選的,因為有的垃圾回收器不會產生記憶體碎片).
新生代中垃圾回收

新生代中用Scavenge 演算法來處理,把新生代空間對半劃分為兩個區域,一半是對象區域,一半是空閑區域。新加入的對象都會存放到對象區域,當對象區域快被寫滿時,就需要執行一次垃圾清理操作。

在垃圾回收過程中,首先要對對象區域中的垃圾做標記;標記完成之後,就進入垃圾清理階段,副垃圾回收器會把這些存活的對象複製到空閑區域中,同時它還會把這些對象有序地排列起來,所以這個複製過程,也就相當於完成了記憶體整理操作,複製後空閑區域就沒有記憶體碎片了。

完成複製後,對象區域與空閑區域進行角色翻轉,也就是原來的對象區域變成空閑區域,原來的空閑區域變成了對象區域。這樣就完成了垃圾對象的回收操作,同時這種角色翻轉的操作還能讓新生代中的這兩塊區域無限重覆使用下去.

為了執行效率,一般新生區的空間會被設置得比較小,也正是因為新生區的空間不大,所以很容易被存活的對象裝滿整個區域。為瞭解決這個問題,JavaScript 引擎採用了對象晉升策略,也就是經過兩次垃圾回收依然還存活的對象,會被移動到老生區中。

老生代中的垃圾回收

老生代中用標記 - 清除(Mark-Sweep)的演算法來處理。首先是標記過程階段,標記階段就是從一組根元素開始,遞歸遍歷這組根元素(遍歷調用棧),在這個遍歷過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾數據.然後在遍歷過程中標記,標記完成後就進行清除過程。它和副垃圾回收器的垃圾清除過程完全不同,這個的清楚過程是刪除標記數據。

清除演算法後,會產生大量不連續的記憶體碎片。而碎片過多會導致大對象無法分配到足夠的連續記憶體,於是又產生了標記 - 整理(Mark-Compact)演算法,這個標記過程仍然與標記 - 清除演算法里的是一樣的,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體,從而讓存活對象占用連續的記憶體塊。

全停頓

由於 JavaScript 是運行在主線程之上的,一旦執行垃圾回收演算法,都需要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢後再恢復腳本執行。我們把這種行為叫做全停頓

在 V8 新生代的垃圾回收中,因其空間較小,且存活對象較少,所以全停頓的影響不大,但老生代就不一樣了。如果執行垃圾回收的過程中,占用主線程時間過久,主線程是不能做其他事情的。比如頁面正在執行一個 JavaScript 動畫,因為垃圾回收器在工作,就會導致這個動畫在垃圾回收過程中無法執行,這將會造成頁面的卡頓現象。

為了降低老生代的垃圾回收而造成的卡頓,V8 將標記過程分為一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,我們把這個演算法稱為增量標記(Incremental Marking)演算法.

使用增量標記演算法,可以把一個完整的垃圾回收任務拆分為很多小的任務,這些小的任務執行時間比較短,可以穿插在其他的 JavaScript 任務中間執行,這樣當執行上述動畫效果時,就不會讓用戶因為垃圾回收任務而感受到頁面的卡頓了。

記憶體泄漏

不再用到的記憶體,沒有及時釋放,就叫做記憶體泄漏(memory leak)。

記憶體泄漏發生的原因

  1. 緩存

有時候為了方便數據的快捷復用,我們會使用緩存,但是緩存必須有一個大小上限才有用。高記憶體消耗將會導致緩存突破上限,因為緩存內容無法被回收。

  1. 隊列消費不及時
    當瀏覽器隊列消費不及時時,會導致一些作用域變數得不到及時的釋放,因而導致記憶體泄漏。

  2. 全局變數

除了常規設置了比較大的對象在全局變數中,還可能是意外導致的全局變數,如:

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

在函數中,沒有使用 var/let/const 定義變數,這樣實際上是定義在window上面,變成了window.bar
再比如由於this導致的全局變數:

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

這種函數,在window作用域下被調用時,函數裡面的this指向了window,執行時實際上為window.bar=xxx,這樣也產生了全局變數。

  1. 計時器中引用沒有清除

先看如下代碼:

var someData = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someData));
    }
}, 1000);

這裡定義了一個計時器,每隔1s把一些數據寫到Node節點裡面。但是當這個Node節點被刪除後,這裡的邏輯其實都不需要了,可是這樣寫,卻導致了計時器裡面的回調函數無法被回收,同時,someData里的數據也是無法被回收的。

  1. 閉包

看以下這個閉包:

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);

每次調用 replaceThingtheThing 會創建一個大數組和一個新閉包(someMethod)的新對象。同時,變數 unused 是一個引用 originalThing(theThing) 的閉包,閉包的作用域一旦創建,它們有同樣的父級作用域,作用域是共用的。

someMethod 可以通過 theThing 使用,someMethodunused 分享閉包作用域,儘管 unused 從未使用,它引用的 originalThing 迫使它保留在記憶體中(防止被回收)。

因此,當這段代碼反覆運行,就會看到記憶體占用不斷上升,垃圾回收器(GC)並無法降低記憶體占用。

本質上,閉包的鏈表已經創建,每一個閉包作用域攜帶一個指向大數組的間接的引用,造成嚴重的記憶體泄漏。

  1. 事件監聽

例如,Node.js 中 Agent 的 keepAlive 為 true 時,可能造成的記憶體泄漏。當 Agent keepAlive 為 true 的時候,將會復用之前使用過的 socket,如果在 socket 上添加事件監聽,忘記清除的話,因為 socket 的復用,將導致事件重覆監聽從而產生記憶體泄漏。

記憶體泄漏的識別方法

  1. 使用 Chrome 任務管理器實時監視記憶體使用
    打開 chrome 瀏覽器,點擊右上角主菜單,選擇更多工具->任務管理器,這樣就開啟了任務管理器面板,然後再右鍵點擊任務管理器的表格標題並啟用 JavaScript使用的記憶體,能看到這樣的面板:

下麵兩列可以告訴您與頁面的記憶體使用有關的不同信息:
GitHub

  1. 記憶體占用空間(Memory) 列表示原生記憶體。DOM 節點存儲在原生記憶體中。 如果此值正在增大,則說明正在創建 DOM 節點。
  2. JavaScript使用的記憶體(JavaScript Memory) 列表示 JS 堆。此列包含兩個值。 您感興趣的值是實時數字(括弧中的數字)。實時數字表示您的頁面上的可到達對象正在使用的記憶體量。 如果此數字在增大,要麼是正在創建新對象,要麼是現有對象正在增長。

當你頁面穩定下來之後,這兩個的值還在上漲,你就可以查一查是否記憶體泄漏了。

  1. 利用chrome 時間軸記錄可視化記憶體泄漏

Performance(時間軸)能夠面板直觀實時顯示JS記憶體使用情況、節點數量、監聽器數量等。

打開 chrome 瀏覽器,調出調試面板(DevTools),點擊Performance選項(低版本是Timeline),勾選Memory覆選框。一種比較好的做法是使用強制垃圾回收開始和結束記錄。在記錄時點擊 Collect garbage 按鈕 (強制垃圾回收按鈕) 可以強制進行垃圾回收。
所以錄製順序可以這樣:開始錄製前先點擊垃圾回收-->點擊開始錄製-->點擊垃圾回收-->點擊結束錄製。
面板介紹如圖:
GitHub
錄製結果如圖:
GitHub
首先,從圖中我們可以看出不同顏色的曲線代表的含義,這裡主要關註JS堆記憶體、節點數量、監聽器數量。滑鼠移到曲線上,可以在左下角顯示具體數據。在實際使用過程中,如果您看到這種 JS 堆大小或節點大小不斷增大的模式,則可能存在記憶體泄漏。

  1. 使用堆快照發現已分離 DOM 樹的記憶體泄漏

只有頁面的 DOM 樹或 JavaScript 代碼不再引用 DOM 節點時,DOM 節點才會被作為垃圾進行回收。 如果某個節點已從 DOM 樹移除,但某些 JavaScript 仍然引用它,我們稱此節點為“已分離”,已分離的 DOM 節點是記憶體泄漏的常見原因。

同理,調出調試面板,點擊Memory,然後選擇Heap Snapshot,然後點擊進行錄製。錄製完成後,選中錄製結果,在 Class filter 文本框中鍵入 Detached,搜索已分離的 DOM 樹。
以這段代碼為例:

<html>
<head>
</head>
<body>
<button id="createBtn">增加節點</button>
<script> 
var detachedNodes;

function create() {
  var ul = document.createElement('ul');
  for (var i = 0; i < 10; i++) {
    var li = document.createElement('li');
    ul.appendChild(li);
  }
  detachedTree = ul;
}

document.getElementById('createBtn').addEventListener('click', create);
</script>
</body>
</html>

點擊幾下,然後記錄。可以得到以下信息:
GitHub
舊版的面板,還會有顏色標註,黃色的對象實例表示它被JS代碼引用,紅色的對象實例表示被黃色節點引用的游離節點。上圖是新版本的,不會有顏色標識。但是還是可以一個個來看,如上圖,點開節點,可以看到下麵的引用信息,上面可以看出,有個HTMLUListElement(ul節點)被window.detachedNodes引用。再結合代碼,原來是沒有加var/let/const聲明,導致其成了全局變數,所以DOM無法釋放。

  1. 按函數調查記憶體分配
    打開面板,點擊JavaScript Profiler,如果沒看到這個選項,你可以點調試面板右上角的三個點,選擇more tools,然後選擇。

ps: chrome 舊版的瀏覽器,這個功能在 Profiles 裡面,點Record Allocation Profile即可.

操作步驟:點start->在頁面進行你要檢測的操作->點stop。
GitHub
DevTools 按函數顯示記憶體分配明細。預設視圖為 Heavy (Bottom Up),將分配了最多記憶體的函數顯示在最上方,還有函數的位置,你可以看看是哪些函數占用記憶體較多。

避免記憶體泄漏的方法

  1. 少用全局變數,避免意外產生全局變數
  2. 使用閉包要及時註意,有Dom元素的引用要及時清理。
  3. 計時器里的回調沒用的時候要記得銷毀。
  4. 為了避免疏忽導致的遺忘,我們可以使用 WeakSetWeakMap結構,它們對於值的引用都是不計入垃圾回收機制的,表示這是弱引用。
    舉個例子:
const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

這種情況下,一旦消除對該節點的引用,它占用的記憶體就會被垃圾回收機制釋放。Weakmap 保存的這個鍵值對,也會自動消失。

基本上,如果你要往對象上添加數據,又不想干擾垃圾回收機制,就可以使用 WeakMap。

參考資料

最後

  • 歡迎加我微信(winty230),拉你進技術群,長期交流學習...
  • 歡迎關註「前端Q」,認真學前端,做個有專業的技術人...

GitHub


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • mysqldump -u root -p 要備份的資料庫名> /home/mysql/backup/db/back/資料庫名.sql ...
  • 集合操作常用的集合操作主要有三種:UNION(聯合集)、INTERSECT(交叉集)、EXCEPT(求差集)。以上三種集合的操作都是直接作用在兩個或者多個 SQL 查詢語句之間,將所有的元組按照特定的要求篩選後拼接起來。SQL 查詢後實際上是得到一個新的數據表的形式,因此所作用的數據表之間必須定義相 ...
  • 摘要: 多條紀錄中,幾個欄位相同,但是其中一個或者多個欄位不同,則去該欄位最大(這裡只有一個不同) 源數據: 目的是移除:在同一天中只能存在一天數據,則取審核日期最大,資料庫腳本如下: 得到的目標結果如下: 總結:該方法使用使用函數和分組;也可以使用分組聚合函數group_concat; 前者簡單, ...
  • https://www.jianshu.com/p/ec2f65523cc4 ...
  • 很簡單的走馬燈效果 關註公眾號 WEB前端大澳 領取資料 ...
  • HTML語法規範 1. 語法規範概述 1. HTML標簽是由尖括弧包圍的關鍵詞,例如 2. HTML標簽通常是成對出現的,例如 和 ,我們成為雙標簽。標簽對中的第一個標簽是開始標簽,第二個標簽是結束標簽。 3. 有些特殊的標簽必須是單個標簽,例如 ,我們稱之為單標簽 2. 標簽關係 包含關係 ~~~ ...
  • // 折線圖let lineChart = echarts.init(document.getElementById('lineChart'));let lineOption = { title: { text: '數據接入增量趨勢', textStyle: { color: '#cccccc', ... ...
  • 這套代碼可以拿過去直接用 一些註意我會在下麵代碼中加上註釋: 謝謝支持 核心代碼 websocket方法調用 此代碼為本博主原創,轉載請註明出處(支持原創! 謝謝~) 湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字數~湊字 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...