1. 程式人生 > >Chrome 瀏覽器垃圾回收機制與記憶體洩漏分析

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

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使用的記憶體,能看到這樣的面板:

下面兩列可以告訴您與頁面的記憶體使用有關的不同資訊:

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

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

  1. 利用chrome 時間軸記錄視覺化記憶體洩漏

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

開啟 chrome 瀏覽器,調出除錯面板(DevTools),點選Performance選項(低版本是Timeline),勾選Memory複選框。一種比較好的做法是使用強制垃圾回收開始和結束記錄。在記錄時點選 Collect garbage 按鈕 (強制垃圾回收按鈕) 可以強制進行垃圾回收。
所以錄製順序可以這樣:開始錄製前先點選垃圾回收-->點選開始錄製-->點選垃圾回收-->點選結束錄製。
面板介紹如圖:

錄製結果如圖:

首先,從圖中我們可以看出不同顏色的曲線代表的含義,這裡主要關注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>

點選幾下,然後記錄。可以得到以下資訊:

舊版的面板,還會有顏色標註,黃色的物件例項表示它被JS程式碼引用,紅色的物件例項表示被黃色節點引用的遊離節點。上圖是新版本的,不會有顏色標識。但是還是可以一個個來看,如上圖,點開節點,可以看到下面的引用資訊,上面可以看出,有個HTMLUListElement(ul節點)被window.detachedNodes引用。再結合程式碼,原來是沒有加var/let/const宣告,導致其成了全域性變數,所以DOM無法釋放。

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

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

操作步驟:點start->在頁面進行你要檢測的操作->點stop。

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。

參考資料

  • 極客時間《瀏覽器工作原理與實踐》
  • https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/
  • https://developers.google.com/web/tools/chrome-devtools/memory-problems?hl=zh-cn

最後

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

相關推薦

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

Chorme 瀏覽器中的垃圾回收和記憶體洩漏 垃圾回收 通常情況下,垃圾資料回收分為手動回收和自動回收兩種策略。 手動回收策略,何時分配記憶體、何時銷燬記憶體都是由程式碼控制的。 自動回收策略,產生的垃圾資料是由垃圾回收器來釋放的,並不需要手動通過程式碼來釋放。 JavaScript 中呼叫棧中的資料回收 J

【達內課程】Android中的GC垃圾回收機制記憶體洩漏

當main()方法執行完,main()方法中的區域性變數都會彈棧,從棧當中銷燬 當左側棧中的e2和e銷燬後,右側中的兩個物件就是垃圾 java底層有一種GC垃圾回收機制,在java程式執行時,GC執行緒會不斷找尋垃圾,是的話會清除掉 當我們點選模擬機的返回鍵時,發生了什麼 當G

JavaScript的垃圾回收機制記憶體洩漏

常用的兩種演算法: 引用計數(新版瀏覽器已棄用,棄用原因:會出現迴圈引用的情況,無法進行垃圾回收,導致記憶體洩漏) 標記清除 引用計數法 引用計數,顧名思義一個物件是否有指向它的引用,即看棧中是否有指向要釋放的該塊堆記憶體中的地址,如果沒有,則該塊記憶體是不需要的,可以進行釋放,即垃圾回收 下面引用大佬的一個

python垃圾回收機制 Java記憶體管理垃圾回收

語言的記憶體管理是語言設計的一個重要方面。它是決定語言效能的重要因素。無論是C語言的手工管理,還是Java的垃圾回收,都成為語言最重要的特徵。這裡以Python語言為例子,說明一門動態型別的、面向物件的語言的記憶體管理方式。 物件的記憶體使用 賦值語句是語言最常見的功能了。但即使是最簡單的賦值語句,也可以

詳解JVM記憶體管理垃圾回收機制1 - 記憶體管理

Java應用程式是執行在JVM上的,得益於JVM的記憶體管理和垃圾收集機制,開發人員的效率得到了顯著提升,也不容易出現記憶體溢位和洩漏問題。但正是因為開發人員把記憶體的控制權交給了JVM,一旦出現記憶體方面的問題,如果不瞭解JVM的工作原理,將很難排查錯誤。本文將從理論角度介紹虛擬機器的記憶

Chrome V8系列--淺析Chrome V8引擎中的垃圾回收機制記憶體洩露優化策略

V8 實現了準確式 GC,GC 演算法採用了分代式垃圾回收機制。因此,V8 將記憶體(堆)分為新生代和老生代兩部分。   一、前言 V8的垃圾回收機制:JavaScript使用垃圾回收機制來自動管理記憶體。垃圾回收是一把雙刃劍,其好處是可以大幅簡化程式的記憶體管理程式碼,降低程式設計師的負擔,

垃圾回收機制內存分配

可用 vivo 比較 固定 類結構 缺點 思路 pau 開發人員 摘要   程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行者出棧和入棧。每個棧幀中分配多少內存基本上是在類結構確定下來時就已知的(盡管在運行期會

JVM 垃圾回收機制GC效能調優

一、GC概要: JVM堆相關知識     為什麼先說JVM堆?     JVM的堆是Java物件的活動空間,程式中的類的物件從中分配空間,其儲存著正在執行著的應用程式用到的所有物件。這些物件的建立方式就是那些new一類的操作,當物件

深入Java虛擬機器閱讀感(二)-Java垃圾回收記憶體分配策略

垃圾回收器主要演算法:       1、引用計數法。給物件新增一個計數器,當物件被使用時則加1,當引用失效時則減1,當計數為0時則認為該物件可以被回收。由於該算演算法無法解決物件相互引用而計數不會減為0,導致該物件無法回收,所以該演算法不是Java虛擬垃圾回收器

Java 垃圾回收機制幾種垃圾回收演算法

一、如何確定某個物件是“垃圾”? 這一小節先了解一個最基本的問題:如果確定某個物件是“垃圾”?既然垃圾收集器的任務是回收垃圾物件所佔的空間供新的物件使用,那麼垃圾收集器如何確定某個物件是“垃圾”?通過什麼方法判斷一個物件可以被回收了。 在java中是通過引用來和物件進行關

JVM的4種垃圾回收演算法、垃圾回收機制總結

本文標題:直通BAT必考題系列:JVM的4種垃圾回收演算法、垃圾回收機制與總結  轉載請保留頁面地址:http://youzhixueyuan.com/jvm-garbage-collection-algorithm.html 垃圾回收演算法 1.標記清除 標記-清除演算

Java垃圾回收機制典型的垃圾回收演算法

說到垃圾回收(Garbage Collection,GC),很多人就會自然而然地把它和Java聯絡起來。在Java中,程式設計師不需要去關心記憶體動態分配和垃圾回收的問題,這一切都交給了JVM來處理。顧名思義,垃圾回收就是釋放垃圾佔用的空間,那麼在Java中,什麼樣的

JVM從零開始 -垃圾回收機制以及記憶體分代模型

JVM中垃圾回收的判定標準 最終目的是將記憶體中無用的物件回收掉。具體的判定方法有: 引用計數法,不採用,指的是維護物件被引用的

Android記憶體管理機制記憶體洩漏分析及優化

Android中的記憶體管理機制 分配機制 Android為每個程序分配記憶體的時候,採用了彈性的分配方式,也就是剛開始並不會一下分配很多記憶體給每個程序,而是給每一個程序分配一個“夠用”的量。這個量是根據每一個裝置實際的實體記憶體大小來決定的。隨著應用

Python記憶體垃圾回收機制

1、python記憶體管理機制和調優手段。   記憶體管理機制:引用計數,垃圾回收、記憶體池。 引用計數:   引用計數是一種非常高效的記憶體管理手段,當一個Python物件被引用時其引用計數增加1,當其不再被一個變數引用時則記數減1,當引用記數等於0時物件被刪除。 垃圾回收:   1、引用計數:

詳解JVM記憶體管理垃圾回收機制2 - 何為垃圾

隨著程式語言的發展,GC的功能不斷增強,效能也不斷提高,作為語言背後的無名英雄,GC離我們的工作似乎越來越遠。作為Java程式設計師,對這一點也許會有更深的體會,我們不需要了解太多與GC相關的知識,就能很好的完成工作。那還有必要深入瞭解GC嗎?學習GC的意義在哪兒? 不管效能提高到何種程

js垃圾回收機制和引起記憶體洩漏的操作

JS的垃圾回收機制瞭解嗎?        Js具有自動垃圾回收機制。垃圾收集器會按照固定的時間間隔週期性的執行。 JS中最常見的垃圾回收方式是標記清除。 工作原理:是當變數進入環境時,將這個變數標記為“進入環境”。當變數離開環境時,則將其標記為“離開環境”。標記

深入理解java虛擬機器---java記憶體區域記憶體溢位異常---3垃圾回收機制GC

  一、垃圾回收---物件存活演算法:     1、引用計數器法:在物件身上放上一個計數器,當有引用則加一,引用失效則減一,為零則可回收。(無法解決物件相互引用)     2、可達性分析法(java),GC roots為起始點,從節點向下搜尋,搜尋路徑為引用鏈,不在引用鏈的物件則是可回收的物件

python中的記憶體管理分析以及垃圾回收機制

1.記憶體分析和處理 程式的執行離不開對記憶體的操作,一個軟體要執行,需要將資料載入到記憶體中,通過CPU進行記憶體資料的讀寫,完成資料的運算。 1.1不可變資料型別VS可變資料型別 python中根據資料是否可以進行修改提供了兩種不同的資料型別 ⚫ 不可變資料

js棧記憶體和堆記憶體垃圾回收機制

v8作為javascirpt引擎,對變數的儲存主要有兩種位置:棧儲存和堆儲存 棧記憶體儲存基本型別的變數:如Number,String,boolen,undefined,null,以及對物件和陣列變數的指標,物件是動態分配記憶體,陣列的初始化方式分為: 動態初始化:初始化