鍍金池/ 教程/ C++/ JavaScript 內(nèi)存分析
谷歌瀏覽器開發(fā)工具綜述
在安卓設(shè)備上使用 Chrome 遠(yuǎn)程調(diào)試功能
命令行 API 參考
快捷鍵
通過工作空間保存更改
展示 Chrome 調(diào)試協(xié)議客戶端實(shí)例
技巧和竅門
控制臺 API 參考
遠(yuǎn)程調(diào)試協(xié)議
Settings
管理應(yīng)用存儲空間
擴(kuò)展 DevTools
遠(yuǎn)程調(diào)試協(xié)議
使用 CSS 預(yù)處理器
分析 JavaScript 性能
使用控制臺
DevTools 插件實(shí)例
使用時(shí)間軸
編輯樣式以及 DOM
郵件列表
樹形提示 (不穩(wěn)定)
時(shí)間軸示例:強(qiáng)制同步布局的診斷
評估網(wǎng)絡(luò)性能
博客帖子
設(shè)備模式&移動仿真
開發(fā)工作流程
視頻 Videos
調(diào)試 JavaScript 腳本
JavaScript 內(nèi)存分析
整合 DevTools
對 Chrome 開發(fā)工具的貢獻(xiàn)

JavaScript 內(nèi)存分析

內(nèi)存泄露是指計(jì)算機(jī)內(nèi)存逐漸丟失。當(dāng)某個程序總是無法釋放內(nèi)存時(shí),就會出現(xiàn)內(nèi)存泄露。JavaScript web 應(yīng)用程序可能會經(jīng)常遇到類似于本地程序中內(nèi)存泄露這樣的問題,比如泄露和膨脹,但是 JavaScript 有內(nèi)存回收機(jī)制可以解決此類問題。

盡管 JavaScript 使用了內(nèi)存回收機(jī)來自動管理內(nèi)存,高效的內(nèi)存管理策略依然是相當(dāng)重要的。在本章中我們會詳細(xì)說明 JavaScript web 應(yīng)用程序中的內(nèi)存問題。在學(xué)習(xí)某些特性的時(shí)候請嘗試這些示例,這可以增進(jìn)你對于工具運(yùn)行原理的認(rèn)識。

在開始之前,請查看 Memory 101 頁面來熟悉一下相關(guān)的專業(yè)術(shù)語。

注意:我們在后面使用的有些特性是只有 Chrome Canary 才支持的。我們建議使用此版本的工具,這樣您就可以對您的應(yīng)用程序做出最佳的內(nèi)存分析。

應(yīng)該問自己的一些問題

通常情況下,當(dāng)你認(rèn)為你的程序出現(xiàn)內(nèi)存泄露的時(shí)候,你需要問自己三個問題:

  • 是不是我的頁面占用了太多的內(nèi)存?- 內(nèi)存時(shí)間軸視圖 以及 Chrome 任務(wù)管理器 可以幫助你來確認(rèn)是否占用了過多的內(nèi)存。內(nèi)存視圖在監(jiān)察過程中可以實(shí)時(shí)跟蹤 DOM 節(jié)點(diǎn)數(shù)目、文件以及 JS 事件監(jiān)聽器。有一條重要法則需要記?。罕苊獗A魧σ呀?jīng)不需要的 DOM 元素的引用,不必要的事件監(jiān)聽器請解除綁定,對于大量的數(shù)據(jù),在存儲時(shí)請注意不要存儲用不到的數(shù)據(jù)。
  • 我的頁面是不是沒有內(nèi)存泄露的問題?- 對象分配跟蹤器能夠讓你看到 JS 對象的實(shí)時(shí)分配過程,以此來降低內(nèi)存泄露的可能。你也可以使用堆探查器來記錄 JS 堆的狀態(tài),然后分析內(nèi)存圖并將其與堆狀態(tài)進(jìn)行比對,就可以迅速發(fā)現(xiàn)那些沒有被垃圾回收器清理的對象。
  • 我的頁面應(yīng)該多久強(qiáng)制進(jìn)行一次垃圾回收? - 如果垃圾回收器總是處于垃圾回收狀態(tài),那么可能是你對象分配過于頻繁了。內(nèi)存時(shí)間軸視圖可以在你感興趣的地方停頓,方便你查看回收情況。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_0.png" alt="image_0" />

視圖內(nèi)容

術(shù)語以及基本原理

這個部分介紹了內(nèi)存分析中常見的術(shù)語,即使是在其他語言的內(nèi)存分析工具中,這些術(shù)語也同樣有用。這里所說的術(shù)語和概念是用于堆探查器界面以及相應(yīng)文檔中的。

了解這些術(shù)語后,你們就能更加高效地使用這個工具。如果你曾經(jīng)使用 Java、.Net 或者其它內(nèi)存分析器,那么該篇的內(nèi)容對你而言就是一次提升。

對象的大小

請將內(nèi)存狀況想象為一副圖片,圖中有著一些基本類型(像是數(shù)字以及字符串等)和對象(關(guān)聯(lián)數(shù)組)。如果像下面這樣將圖中的內(nèi)容用一些相互連接的點(diǎn)來表示,可能有助于你對此的理解:

http://wiki.jikexueyuan.com/project/chrome-devtools/images/thinkgraph.png" alt="thinkgraph" />

對象可以通過兩種方式來獲取內(nèi)存:

  • 直接通過它本身。
  • 通過包含對其它對象的引用,這樣就會阻止垃圾回收器(簡稱 GC)自動回收這些對象。

當(dāng)使用 DevTools 中的堆分析器(一種用于查找“配置文件”下的內(nèi)存問題的工具)的時(shí)候,你會發(fā)現(xiàn)你所看到的是幾列信息。其中最重要的就是 Shallow Size 以及 Retained Size,不過,這兩列究竟意味著什么呢?

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_1.png" alt="images" />

Shallow size

這是指對象本身獲得的內(nèi)存大小。

典型的 JavaScript 對象會獲得一些保留的內(nèi)存,用于他們的描述以及存儲即時(shí)產(chǎn)生的值。通常情況下,只有數(shù)組和字符串才會有比較明顯的淺層大小。不過,字符串和外部數(shù)組往往在渲染內(nèi)存中有它們自己的主存儲器,對 JavaScript 堆只露出一點(diǎn)包裝后的對象。

渲染內(nèi)存是指所監(jiān)視的頁面被渲染的過程中使用的內(nèi)存:原本分配的內(nèi)存 + 該頁面在 JS 堆中的內(nèi)存 + 所有因?yàn)樵擁撁娑鴮?dǎo)致的 JS 堆中其他對象的內(nèi)存開銷。然而,即使是一個小的對象也可以通過阻止垃圾回收器自動回收其他對象來間接保有大量的內(nèi)存。

Retained size

這是指對象以及其相關(guān)的對象一起被刪除后所釋放的內(nèi)存大小,并且 GC roots 無法到達(dá)該處。

GC roots 是由在從原生代碼的 V8 之外引用 JavaScript 對象的時(shí)候所創(chuàng)建的句柄(局部或者全局的)構(gòu)成的。這些句柄可以再堆的快照中 GC roots > Handle scope 以及 GC roots > Global handles 中找到。在沒有談及瀏覽器實(shí)現(xiàn)的細(xì)節(jié)的情況下,就在本文中說明句柄會令讀者感到困惑,故而關(guān)于句柄的細(xì)節(jié)本文不做講解。事實(shí)上,無論 GC roots 還是句柄,都不是你需要擔(dān)心的東西。

內(nèi)部的 GC roots 有很多,不過用戶對其中的大部分都不感興趣。從應(yīng)用程序的角度來說,有下面這么幾種 roots:

  • 窗口全局對象(在每一幀中)。在堆快照中,有一個距離域,其包含的是在窗口最短保留路徑上的屬性引用的數(shù)目。
  • 文檔 DOM 樹是由所有分析該文檔時(shí)能夠到達(dá)的 DOM 節(jié)點(diǎn)構(gòu)成的。并不是所有的節(jié)點(diǎn)都會有 JS 封裝,但是如果他們有封裝,那么只要文檔還在,這些節(jié)點(diǎn)就可以使用。
  • 有些時(shí)候,對象會被調(diào)試器上下文以及 DevTools 控制臺保留。(例如,在控制臺進(jìn)行評估后)

注意:我們推薦讀者在清空控制臺并且調(diào)試器中沒有活躍的斷點(diǎn)的情況下來做堆的快照。

下面的內(nèi)存就是由一個根節(jié)點(diǎn)開始的,這個根節(jié)點(diǎn)可能是瀏覽器的 window 對象或者是 Node.js 模塊的 Global 對象。你并不需要知道這個對象是如何被回收的。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/dontcontrol.png" alt="dontcontrol" />

任何無法被根節(jié)點(diǎn)取得的元素夠?qū)⒈换厥铡?/p>

提示:Shallow 和 Retained size 都用字節(jié)來表示數(shù)據(jù)。

對象的保留樹

就像我們前面所說的,堆就是由相互連接的對象構(gòu)成的網(wǎng)絡(luò)。在數(shù)學(xué)的世界中,這種結(jié)構(gòu)稱作或者內(nèi)存圖。一個圖是由節(jié)點(diǎn)和邊構(gòu)成的,而節(jié)點(diǎn)又是由邊連接起來的,其中節(jié)點(diǎn)和邊都有相應(yīng)的標(biāo)簽。

  • 節(jié)點(diǎn)(或者對象)是用創(chuàng)建對象的構(gòu)造函數(shù)標(biāo)記的。
  • 是用屬性名來標(biāo)記的。

在本文后面的內(nèi)容中,你將會學(xué)到如何使用堆探查器來記錄資料。在堆分析器記錄中我們可以看到包括 Distance 在內(nèi)的幾欄:Distance 指的是從根節(jié)點(diǎn)到當(dāng)前節(jié)點(diǎn)的距離。有一種情況是值得探究的,那就是幾乎所有同類的對象都有著相同的距離,但是有一小部分對象的 Distance 的值要比其他對象大一些。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_2.png" alt="images" />

主導(dǎo)者

主導(dǎo)者對象是由樹形結(jié)構(gòu)組成的,因?yàn)槊總€對象都只有一個主導(dǎo)者。一個對象的支配者不一定直接引用它所主導(dǎo)的對象,也就是說,支配樹并不是圖的生成樹。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/dominatorsspanning.png" alt="dominatorsspanning" />

在上面的圖中:

  • 節(jié)點(diǎn) 1 主導(dǎo)了節(jié)點(diǎn) 2.
  • 節(jié)點(diǎn) 2 主導(dǎo)了節(jié)點(diǎn) 3,4,6
  • 節(jié)點(diǎn) 3 主導(dǎo)了節(jié)點(diǎn) 5
  • 節(jié)點(diǎn) 5 主導(dǎo)了節(jié)點(diǎn) 8
  • 節(jié)點(diǎn) 6 主導(dǎo)了節(jié)點(diǎn) 7

在下面的例子中,節(jié)點(diǎn) #3#10 的主導(dǎo)者,但是 #7 節(jié)點(diǎn)也在由 GC 到 #10 節(jié)點(diǎn)的,每條簡單路徑上。因此,如果對象 B 存在于從根節(jié)點(diǎn)到對象 A 的,每條簡單路徑上,那么對象 B 就是對象 A 的主導(dǎo)者。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/dominator.gif" alt="dominator" />

V8 的細(xì)節(jié)

在本節(jié)中,我們所講的是對應(yīng) V8 JavaScript 虛擬機(jī)(V8 VM 或者 VM)的內(nèi)存方面的話題。這些內(nèi)容對于理解堆快照為何是上面所看到的那個樣子很有幫助。

JavaScript 對象的表示

JavaScript 中有三種主要類型:

  • 數(shù)字(比如,3.14159..)
  • 布爾值(true 或者 false)
  • 字符串 (比如 "Werner Heisenberg")

這些類型在樹中都是葉子節(jié)點(diǎn)或者終結(jié)節(jié)點(diǎn),并且它們不能引用其它值。

數(shù)字類型可以像下面這樣存儲:

  • 相鄰的 31 位整數(shù)值,被稱為 small integers (SMIs)
  • 被稱為堆數(shù)字的堆對象。堆數(shù)字用于存儲不適合 SMI 形式的值,比如浮點(diǎn)類型,或者是需要封裝的值,比如設(shè)置其屬性值的類型。

字符串可以被存儲在:

  • 虛擬機(jī)的堆
  • 外部的渲染內(nèi)存。也就是當(dāng)創(chuàng)建或者使用一個封裝后的對象時(shí)需要使用的外部存儲器,比如,腳本資源以及其他從網(wǎng)上接收而不是賦值到虛擬機(jī)堆中存儲的內(nèi)容。

新的 JavaScript 對象的內(nèi)存是由特定的 JavaScript 堆(或者說 VM 堆)分配的。這些對象由 V8 垃圾回收器管理,并且只要存在一個對他們的強(qiáng)引用就不會被回收。

本地對象指的是不在 JavaScript 堆中存儲的一切對象。本地對象和堆對象相反,其生存周期不由 V8 垃圾回收器管理,并且只能通過封裝它們的 JavaScript 對象來使用。

Cons string 是一個保存了成對字符串的對象,并且該對象會將字符串拼接起來,最后的結(jié)果是串聯(lián)后的字符串。拼接后的 cons string 的內(nèi)容只有在需要的時(shí)候才會出現(xiàn)。一個比較好的例子就是,如果想獲取某個字符串的子串,就必須利用函數(shù)進(jìn)行構(gòu)建。

舉個例子,如果你將 ab 對象串聯(lián),那么你將獲得一個字符串(a,b) 用于表示拼接后的結(jié)果。如果你之后又加入了一個對象 d,那么你將活的另一個字符串((a,b),d)。

數(shù)組 - 一個數(shù)組就是有著數(shù)字鍵的對象。他們廣泛應(yīng)用在 V8 VM 中,用于存儲大量數(shù)據(jù)。在字典這樣的數(shù)據(jù)結(jié)構(gòu)中鍵值對的集合就是利用數(shù)組來備份的。

一個典型的用于存儲的 JavaScript 對象可以是下列兩種數(shù)組類型之一:

  • 命名的屬性
  • 數(shù)字元素

如果想要存儲的是少量的屬性,那么它們可以直接在 JavaScript 對象中存儲。

Map - 一個對象,用于描述對象及其布局的種類。舉個例子,maps 用于描述快速屬性訪問的隱式對象結(jié)構(gòu)。

對象組

每個本地的對象組都是由保持彼此相互引用的對象組成的。以一個 DOM 子樹為例,在該樹中,每一個節(jié)點(diǎn)都一個指向父節(jié)點(diǎn)的連接,以及指向孩子節(jié)點(diǎn)和兄弟節(jié)點(diǎn)的鏈接,由此,所有的節(jié)點(diǎn)連成了一張圖。需要注意的是,本地對象并不會在 JavaScript 堆中出席那,所以它們的大小是 0。相應(yīng)的,對于每個要使用本地對象都會創(chuàng)建一個對應(yīng)的封裝對象。

每個封裝對象都含有一個對相應(yīng)的本地對象的引用,這是為了能夠?qū)⒚钪囟ㄏ虻奖镜貙ο笊稀6鴮ο蠼M則含有這些封裝的對象,但是,這并不會造成一個無法回收的死循環(huán),因?yàn)槔厥掌鲿詣俞尫挪辉谝玫姆庋b對象。但是一旦忘記了釋放某個封裝對象就可能造成整個組以及相關(guān)封裝對象都無法被釋放。

先決條件以及一些有用的提示

Chrome 任務(wù)管理器

注意:在 Chrome 中分析內(nèi)存問題時(shí),一個比較好的方法就是配置 clean-room testing 環(huán)境

如果某個頁面消耗了大量內(nèi)存,可以在執(zhí)行有可能占用大量內(nèi)存的活動時(shí)使用 Chrome 任務(wù)管理器的內(nèi)存這一欄來監(jiān)視頁面所占用的內(nèi)存。如果要使用任務(wù)管理器,點(diǎn)擊 menu > Tools 或者使用快捷鍵 Shift + Esc。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_5.png" alt="image" />

打開之后,右鍵點(diǎn)擊列頭部分然后啟用 JavaScript memory 列。

使用 DevTools 時(shí)間軸來找出內(nèi)存問題

要解決問題的第一步就是要先擁有找出問題的能力。這意味著能夠創(chuàng)建一個用于基本問題測量的可重復(fù)性測試。如果沒有一個可復(fù)用的程序,你就沒辦法有效地衡量問題。另外,如果連測試基線都沒有的話,就沒辦法知道做出的改變是否提高了程序的性能。

時(shí)間軸面板對于發(fā)現(xiàn)問題出現(xiàn)的時(shí)間非常有幫助。頁面或者應(yīng)用程序加載或者進(jìn)行交互時(shí),它會給出整個流程的時(shí)間消耗的完整概述。所有的事件,從加載資源到解析 JavaScript、計(jì)算樣式、垃圾回收以及重繪都會出現(xiàn)在時(shí)間軸上。

在尋找內(nèi)存問題的時(shí)候,時(shí)間軸面板的 Memory view 可以用來追溯:

  • 總共分配的內(nèi)存 - 內(nèi)存的使用量是否增長了?
  • DOM 節(jié)點(diǎn)的數(shù)量。
  • 文檔的數(shù)量
  • 分配的事件監(jiān)聽器的數(shù)量。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_6.png" alt="image" />

想要了解在內(nèi)存分析時(shí)找出可能造成內(nèi)存泄露的問題的更多信息,請查看 Zack Grossbart 寫的 Memory profiling with the Chrome DevTools

驗(yàn)證存在的問題

首先要做的事情就是找出你認(rèn)為可能造成內(nèi)存泄露的活動。這種活動可能是任何事情,就像是在站點(diǎn)上進(jìn)行定位、鼠標(biāo)的懸停事件、點(diǎn)擊事件或者是與頁面交互時(shí)可能對性能產(chǎn)生消極影響的事件。

在時(shí)間軸面板中,開始記錄(Ctrl + E 或者 Cmd + E)然后執(zhí)行你想測試的活動序列。要強(qiáng)制進(jìn)行垃圾回收,點(diǎn)擊底部的垃圾圖標(biāo)(http://wiki.jikexueyuan.com/project/chrome-devtools/images/collect-garbage.png" alt="collect-garbage" />)。

在下圖中我們可以發(fā)現(xiàn)有些節(jié)點(diǎn)沒有被回收,而這些節(jié)點(diǎn)所對應(yīng)的圖案就是內(nèi)存泄露的圖案樣式:

http://wiki.jikexueyuan.com/project/chrome-devtools/images/nodescollected.png" alt="nodescollect" />

如果在幾次迭代后你看見了一個鋸齒形的圖案(在內(nèi)存面板的頂部),這就說明你分配了大量短生存期的對象。但是,如果這個操作序列并沒有使內(nèi)存保留下來,或者 DOM 節(jié)點(diǎn)的數(shù)量并沒有下降到剛開始執(zhí)行時(shí)的那個基線上,那么你有很好的理由來懷疑這里發(fā)生了內(nèi)存泄露。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_10.png" alt="image" />

一旦你確認(rèn)了存在問題,你就可以借助 Profiles panel 中的 heap profiler 找出問題的來源。

示例:你可以嘗試一下這個例子來鍛煉一下如何高效使用時(shí)間軸內(nèi)存模式。

垃圾回收

垃圾回收器(就像是 V8)能夠定位到你的程序處于生存期的對象以及已經(jīng)死亡的對象,甚至是無法訪問到的對象。

如果垃圾回收器(GC)由于某些邏輯錯誤沒能回收你的 javaScript 中已死亡的對象,那么它們所消耗的內(nèi)存將無法被再次使用。像這樣的情況最終會隨著時(shí)間推移而使得你的應(yīng)用程序的執(zhí)行速率不斷變慢。

如果你在編寫代碼時(shí),即使是不再需要的變量以及事件監(jiān)聽器依舊被其他代碼所引用,最終就會出現(xiàn)這種情況。當(dāng)這些引用存在的時(shí)候,垃圾回收器就沒辦法正確清理這些對象。

在你的應(yīng)用程序的生存期間會有一些 DOM 元素更新/死亡,別忘了檢出并消除引用了這些元素的變量。檢查可能引用了其他對象(或者其他 DOM 元素)的對象的屬性,并留意可能隨著時(shí)間的推移不斷增長的變量緩存。

堆分析器

生成快照

在配置面板中,選擇 Take Heap Snapshot,然后點(diǎn)擊 Start 或者使用 Cmd + ECtrl + E 快捷鍵。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_11.png" alt="image" />

最初快照是存在渲染內(nèi)存中的,當(dāng)你點(diǎn)擊快照圖標(biāo)來查看它的時(shí)候,它將會被傳輸?shù)?DevTools 中。當(dāng)快照載入到 DevTools 中并被解析后,快照標(biāo)題下面會出現(xiàn)一個數(shù)字,該數(shù)字表示所有可訪問的 JavaScript 對象的總大?。?/p>

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_12.png" alt="image" />

示例:嘗試使用這個例子來監(jiān)測時(shí)間軸匯總內(nèi)存的使用情況。

清除快照

點(diǎn)擊清除全部配置圖標(biāo)(http://wiki.jikexueyuan.com/project/chrome-devtools/images/clear.png" alt="image" />)可以清楚快照(DevTools 中和渲染內(nèi)存中都會刪除掉):

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_15.png" alt="image" />

注意:直接關(guān)閉 DevTools 窗口并不會刪除渲染內(nèi)存中的配置文件。當(dāng)重新打開 DevTools 窗口的時(shí)候,所有之前生成的快照都會在快照列表中出現(xiàn)。

記得之前文章中提到過,你可以從 DevTools 中強(qiáng)制進(jìn)行垃圾回收,并且這可以成為你的快照工作流中的一部分。當(dāng)生成一個堆快照的時(shí)候,DevTools 會自動進(jìn)行垃圾回收。在時(shí)間軸中該過程可以通過點(diǎn)擊垃圾桶按鈕(http://wiki.jikexueyuan.com/project/chrome-devtools/images/collect-garbage.png" alt="collect-garbage" />)輕松實(shí)現(xiàn)。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/force.png" alt="force" />

示例:嘗試這個例子并使用堆分析器來進(jìn)行分析。你應(yīng)該看到(對象)項(xiàng)目分配次數(shù)。

在快照視圖間切換

一份快照可以用不同的視角來查看,這樣可以更好地適應(yīng)不同的需求。要在視圖間切換,使用視圖底部的選擇器:

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_17.png" alt="image" />

一共有三種默認(rèn)視圖:

  • 總結(jié) - 通過構(gòu)造器的名稱來分組顯示對象
  • 比較 - 顯示兩份快照間的不同之處
  • 包含 - 允許查看堆中的內(nèi)容

在設(shè)置面板中可以啟用主導(dǎo)視圖 - 顯示了主導(dǎo)樹的內(nèi)容,并且可以用于找到聚集點(diǎn)。

查看代碼顏色

對象的屬性以及屬性值屬于不同類型并且有著相應(yīng)的顏色。每個屬性都會有四種類型之一:

  • a:property - 有名稱的常規(guī)屬性,通過 .(點(diǎn))操作符或者 [](方括號)符號來訪問,例如 ["foo bar"];
  • 0:element - 有數(shù)字下標(biāo)的常規(guī)屬性,使用 [](方括號)來訪問。
  • a:context var - 函數(shù)上下文中的某個變量,在相應(yīng)的函數(shù)閉包中使用其名字就可以訪問。
  • a:system prop - 由 JavaScript 虛擬機(jī)添加的屬性,在 JavaScript 代碼中無法訪問。

被命名為 System 這樣的對象是沒有相應(yīng)的 JavaScript 類型的。他們是 JavaScript 虛擬機(jī)的對象系統(tǒng)的一部分。V8 將大多數(shù)內(nèi)部對象分配到和用戶 JS 對象相同的堆中,所以這些都只是 V8 內(nèi)部內(nèi)容。

找到特定對象

要在堆中找到某個對象,你可以使用 Ctrl + F 來打開搜索框,然后輸入對象的 ID。

視圖的詳細(xì)內(nèi)容

總結(jié)視圖

最開始的時(shí)候,快照是在總結(jié)視圖中打開的,顯示了對象的整體情況,并且該視圖可以展開以顯示實(shí)例信息:

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_19.png" alt="image" />

頂級入口是 "total" 行,他們展示了:

  • 構(gòu)造器,表示所有用這個構(gòu)造器創(chuàng)建的對象。
  • 對象實(shí)例的數(shù)量顯示在 # 這一列下。
  • Shallow size 這一列顯示了當(dāng)前構(gòu)造器創(chuàng)建的所有對象的 shallow size 總和。
  • Retained size 這一列顯示相同的對象集所對應(yīng)的最大 retained size。
  • Distance 顯示了從根節(jié)點(diǎn)開始,從節(jié)點(diǎn)的最短路徑到達(dá)當(dāng)前節(jié)點(diǎn)的距離。

想上圖那樣展開 total line 之后,其所有的實(shí)例都會顯示出來。對于每個實(shí)例,它的 shallow size 和 retained size 都會在相應(yīng)列中展示出來。在 @ 字符后面的數(shù)字就是對象的 ID,該 ID 允許你在每個對象的基礎(chǔ)上比較堆的快照。

示例:通過這個頁面來了解如何使用總結(jié)視圖。

請記住,黃色的對象表示有 JavaScript 對象引用了它們,而紅色的對象是指從一個黃色背景節(jié)點(diǎn)引用的分離節(jié)點(diǎn)。

比較視圖

這個視圖用于比較不同的快照,這樣,你就可以通過比較它們的不同之處來找出出現(xiàn)內(nèi)存泄露的對象。想要弄清楚一個特定的程序是否造成了泄露(比如,通常是相對的兩個操作,就像是打開文檔,然后關(guān)閉它,是不會留下內(nèi)存垃圾的),你可以嘗試下列步驟:

  • 在執(zhí)行操作前先生成一份快照。
  • 執(zhí)行操作(該操作涉及到你認(rèn)為出現(xiàn)內(nèi)存泄露的頁面)。
  • 執(zhí)行一個相對的操作(做出相反的交互行為,并重復(fù)多次)。
  • 生成第二份快照然后將視圖切換到比較視圖,將它與第一份快照對比。

在比較視圖中,兩份快照間的不同之處會展示出來。當(dāng)展開一個總?cè)肟跁r(shí),添加以及刪除的對象實(shí)例會顯示出來:

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_21.png" alt="image" />

示例:嘗試這個例子(在選項(xiàng)卡中打開)來了解如何使用比較視圖來監(jiān)測內(nèi)存泄露。

包含視圖

包含視圖本質(zhì)上就像是你的應(yīng)用程序?qū)ο蠼Y(jié)構(gòu)的俯視圖。它使你能夠查看到函數(shù)閉包內(nèi)部,甚至是觀察到那些組成 JavaScript 對象的虛擬機(jī)內(nèi)部對象,借助該視圖,你可以了解到你的應(yīng)用底層占用了多少內(nèi)存。

這個視圖提供了多個接入點(diǎn):

  • DOMWindow objects - 這些是被認(rèn)作“全局”對象的對象。
  • GC roots - 虛擬機(jī)垃圾回收器實(shí)際實(shí)用的垃圾回收根節(jié)點(diǎn)。
  • Native objects - 指的是“推送”到 JavaScript 虛擬機(jī)內(nèi)以實(shí)現(xiàn)自動化的瀏覽器對象,比如,DOM 節(jié)點(diǎn),CSS 規(guī)則(詳細(xì)內(nèi)容請見下一節(jié))

下面是常見的包含視圖的例子:

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_22.png" alt="image" />

示例:通過這個頁面(在新的選項(xiàng)卡中打開)來嘗試如何在該視圖中找到閉包和事件處理器。

關(guān)于閉包的小提示

為函數(shù)命名有助于你在快照中分辨不同的閉包。舉個例子,下面這個函數(shù)沒有命名:

function createLargeClosure() {  
    var largeStr = new Array(1000000).join('x');
    var lC = function() { 
        // this is NOT a named function
        return largeStr;  
    };  
    return lC;
}

而下面這個是命名后的函數(shù):

function createLargeClosure() 
{  
    var largeStr = new Array(1000000).join('x');
    var lC = function lC() { 
        // this IS a named function    
        return largeStr;  
    };  return lC;
}

http://wiki.jikexueyuan.com/project/chrome-devtools/images/domleaks.png" alt="domleaks" />

示例:嘗試一下這個例子來分析閉包對內(nèi)存的影響。你可能會對下面這個例子感興趣,它可以讓你深入了解堆內(nèi)存分配

發(fā)現(xiàn) DOM 內(nèi)存泄露

該工具的一大特點(diǎn)就是它能夠顯示瀏覽器本地對象(DOM 結(jié)點(diǎn),CSS 規(guī)則)以及 JavaScript 對象間的雙向依賴關(guān)系。這有助于發(fā)現(xiàn)因?yàn)橥浄蛛x DOM 子樹而導(dǎo)致的不可見的泄露。

DOM 泄露肯能比你想象中的要多。考慮下面這個例子 - 什么時(shí)候 #tree 會被回收?

var select = document.querySelector;  
var treeRef = select("#tree");  
var leafRef = select("#leaf");  
var body = select("body");
body.removeChild(treeRef);  //#tree can't be GC yet due to treeRef  
treeRef = null;  //#tree can't be GC yet due to indirect  
//reference from leafRef  
leafRef = null;  //#NOW can be #tree GC

#leaf 包含了對其父親(父節(jié)點(diǎn))的引用并遞歸到 #tree,所以只有當(dāng) leafRef 失效的時(shí)候 #tree 下的整棵樹才能被回收。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/treegc.png" alt="treegc" />

示例:嘗試這個例子有助于你理解 DOM 節(jié)點(diǎn)中哪里容易出現(xiàn)泄露以及如何找到它們。你也可以繼續(xù)嘗試后面這個例子DOM 泄露斌想象的要更多。

想要了解更多關(guān)于 DOM 泄露以及內(nèi)存分析的基礎(chǔ)內(nèi)容,請參閱 Gonzalo Ruiz de Villa 編寫的 Finding and debugging memory leaks with the Chrome DevTools。

總結(jié)視圖和包含視圖更加容易找到本地對象 - 在視圖中有對應(yīng)本地對象的入口節(jié)點(diǎn):

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_24.png" alt="image" />

示例:嘗試這個示例(在新選項(xiàng)卡中打開)來體驗(yàn)分離的 DOM 樹。

主導(dǎo)視圖

主導(dǎo)視圖顯示了堆圖的主導(dǎo)樹,從形式上來看,主導(dǎo)視圖有點(diǎn)像是包含視圖,但是缺少了某些屬性。這是因?yàn)橹鲗?dǎo)者對象可能會缺少對它的直接引用,也就是說,主導(dǎo)樹不是生成樹。

注意:在 Chrome Canary 中,主導(dǎo)視圖可以在 Settings > Show advance snapshots properties 中啟用,重啟瀏覽器之后就可以選擇主導(dǎo)視圖了。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_25.png" alt="image" />

示例:嘗試這個例子(在新選項(xiàng)卡中打開)來看看你能不能找到積累點(diǎn)。隨后可以嘗試運(yùn)行 retainning paths and dominators。

對象分配追蹤器

對象追蹤器結(jié)合了堆分析器中快照的詳細(xì)信息以及時(shí)間軸的增量更新以及追蹤信息。跟這些工具相似,追蹤對象堆的分配過程包括開始記錄,執(zhí)行一系列操作,以及停止記錄并分析。

對象分析器在記錄中周期性生成快照(大概每 50 毫秒就會生成一次),并且在記錄最后停止時(shí)也會生成一份快照。堆分配配置文件顯示了對象在哪里創(chuàng)建并且標(biāo)識出了保留路徑。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_26.png" alt="image" />

開啟并使用對象追蹤器

要開始使用對象追蹤器:

  1. 確認(rèn)你安裝了最新的 Chrome Canary
  2. 打開 DevTools 并點(diǎn)擊右邊下面的齒輪圖標(biāo)。
  3. 現(xiàn)在,在配置面板中,你可以看見一項(xiàng)名為 "Record Heap Allocations" 的配置。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_27.png" alt="image" />

頂欄的條形圖表示對象什么時(shí)候在堆中被找到。每個條形圖的高度對應(yīng)最近分配的對象的大小,而其顏色則說明這些對象在最后的快照中是否還處于生存周期:藍(lán)色表示在時(shí)間軸的最后該對象依舊存在,灰色則說明對象在時(shí)間軸內(nèi)被分配,但是已經(jīng)被垃圾回收器回收了。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/collected.png" alt="collected" />

在上面的例子中,一個操作被執(zhí)行了10次。這個簡單的程序加載了五個對象,所以顯示了五個藍(lán)色的條形圖案。但是最左邊的條形圖表明了一個潛在的問題。接下來你可以使用時(shí)間軸中的滑動條來放大這一特定的快照,然后查看最近被分配到這一點(diǎn)上的對象。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_29.png" alt="image" />

點(diǎn)擊堆中的某個特定對象會在堆快照的頂部顯示其保留樹。檢查對象的保留路徑會讓你明白為什么對象沒有被回收,并且你可以在代碼中做出變動來一出不需要的引用。

內(nèi)存分析的問題

Q:我并沒有看到對象的所有屬性,我也沒看到那些非字符串 的值,為什么?

不是所有的屬性都儲存在 JavaScript 堆中。其中有些是通過執(zhí)行了本地代碼的獲取器來實(shí)現(xiàn)的。這樣的屬性不會在堆快照中被捕獲,因?yàn)橐苊庹{(diào)用獲取器的消耗并且要避免程序聲明的變化(當(dāng)獲取器不是“純”方法的時(shí)候)。同樣的,非字符串值,像是數(shù)字等為了縮小快照的大小也沒有捕獲。

Q:在 *@* 字符后面的數(shù)字意味著什么 - 這是一個地址或者 ID 嗎?ID 的值是不是唯一的?

這是對象 ID。顯示對象的地址毫無意義,因?yàn)閷ο蟮牡刂吩诶厥掌陂g會發(fā)生偏移。這些對象 ID 是真正的 ID - 也就是說,他們在生存的多個快照都會存在,并且其值是唯一的。這就使得你可以精確地比較兩個不同時(shí)期的堆狀態(tài)。維護(hù)這些 ID 增加了垃圾回收周期的開銷,但是這只在第一份堆快照生成后才初始化 - 如果堆配置文件沒有使用到的話,就沒有開銷。

Q:“死亡”的(無法到達(dá))對象是否會包含在快照中?

不會,只有可到達(dá)的對象才會在快照中出現(xiàn)。并且,生成一份快照的時(shí)候總是會先開始進(jìn)行垃圾回收。

注意:在編寫代碼的時(shí)候,我們希望避免這種垃圾回收方式以減少在生成堆快照時(shí),已使用的堆大小的變動。這個還在實(shí)現(xiàn)中,但是垃圾回收依舊會在快照之外執(zhí)行。

Q:GC 根節(jié)點(diǎn)是由什么組成的?

許多東西:

  • 內(nèi)置的對象映射
  • 符號表
  • 虛擬機(jī)線程棧
  • 編譯緩存
  • 處理范圍
  • 全局句柄

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_30.jpg" alt="image" />

Q:教程中說使用堆分析器以及時(shí)間軸內(nèi)存視圖來查找內(nèi)存泄露。首先應(yīng)該使用什么工具呢?

時(shí)間軸,使用該工具可以在你意識到頁面開始變慢的時(shí)候檢測出過高的內(nèi)存使用量。速度變慢是典型的內(nèi)存泄露癥狀,當(dāng)然也有可能是由其他情況造成的 - 也許你的頁面中有一些圖片或者是網(wǎng)絡(luò)存在瓶頸,所以要確認(rèn)你是否修復(fù)了實(shí)際的問題。

要診斷內(nèi)存是不是造成問題的原因,打開時(shí)間軸面板的內(nèi)存視圖。點(diǎn)擊紀(jì)錄按鈕然后開始與程序交互,重復(fù)你覺得出現(xiàn)問題的操作。停止記錄,顯示出來的圖片表示分配給應(yīng)用程序的內(nèi)存狀態(tài)。如果圖片顯示消耗的內(nèi)存總量一直在增長(繼續(xù)沒有下落)則說明很有可能出現(xiàn)了內(nèi)存泄露。

一個正常的應(yīng)用,其內(nèi)存狀態(tài)圖應(yīng)該是一個鋸齒形的曲線圖,因?yàn)閮?nèi)存分配后會被垃圾回收器回收。這一點(diǎn)是毋庸置疑的 - 在 JavaScript 中的操作總會有所消耗,即使是一個空的 requestAnimationFrame 也會出現(xiàn)鋸齒形的圖案,這是無法避免的。只要確保沒有尖銳的圖形,就像是大量分配這樣的情況就好,因?yàn)檫@意味著在另一側(cè)會產(chǎn)生大量的垃圾。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_31.png" alt="image" />

你需要在意的是,這條曲線陡度的增加速率。在內(nèi)存視圖中,還有DOM 節(jié)點(diǎn)計(jì)數(shù)器,文檔計(jì)數(shù)器以及事件監(jiān)聽計(jì)數(shù)器,這些在診斷中都是非常有用的。DOM 節(jié)點(diǎn)使用原生內(nèi)存,并且不會直接影響到 JavaScript 內(nèi)存圖表。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_32.jpg" alt="image" />

如果你感覺程序中出現(xiàn)了內(nèi)存泄露,堆分析器可以幫助你找到內(nèi)存泄露的來源。

Q:我注意到在堆快照中有一些 DOM 節(jié)點(diǎn),其中有些是紅色的并且表明是 “分離的 DOM 樹” 而其他的是黃色的,這意味著什么?

你會注意到這些節(jié)點(diǎn)有著不同的顏色,紅色的節(jié)點(diǎn)(其背景較暗)沒有 JavaScript 對其的直接引用,但是依舊處于生存期,因?yàn)樗麄兪欠蛛x的 DOM 樹的一部分。可能會有一些節(jié)點(diǎn)在 JavaScript 引用的樹中(可能是閉包或者變量)但是卻剛好阻止了整棵 DOM 樹被回收。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_33.jpg" alt="image" />

黃色的節(jié)點(diǎn)(其背景也是黃色的)則是有 JavaScript 對象直接引用的。在同一個分離 DOM 樹中查找黃色節(jié)點(diǎn)來鎖定 JavaScript 中的引用。從 DOM 窗口到達(dá)相關(guān)元素應(yīng)該是一條屬性鏈(比如,window.foo.bar[2].baz

下面是關(guān)于獨(dú)立節(jié)點(diǎn)在整幅圖中位置的一個動畫:

http://wiki.jikexueyuan.com/project/chrome-devtools/images/detached-node.gif" alt="detached-node" />

例子:嘗試這個關(guān)于獨(dú)立節(jié)點(diǎn)例子,通過這個例子你可以看到節(jié)點(diǎn)在時(shí)間軸中的變化過程,并且你可以生成堆快照來找到獨(dú)立節(jié)點(diǎn)。

Q:Shallow 以及 Retained Size 表示什么?它們之間有什么區(qū)別?

實(shí)際上,對象在內(nèi)存中的停留是有兩種方式的 - 通過一個其他處于生存期的對象直接保留在內(nèi)存中(比如 window 和 document 對象)或者通過保留對本地渲染內(nèi)存中某些部分的引用而隱式地保留在內(nèi)存中(就像 DOM 對象)。后者會導(dǎo)致相關(guān)的對象無法被內(nèi)存回收器自動回收,最終造成泄漏。而對象本身含有的內(nèi)存大小則是 shallow size(一般來說數(shù)組和字符串有著比較大的 shallow size)。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_36.jpg" alt="image" />

如果某個對象阻止了其他對象被回收,那么不管這個對象有多大,它所占用的內(nèi)存都將是巨大的。當(dāng)一個對象被刪除時(shí)可以回收的內(nèi)存大小則被稱為保留量。

Q:在構(gòu)建器以及保留視圖中有大量的數(shù)據(jù)。如果我發(fā)現(xiàn)存在泄漏的時(shí)候,應(yīng)該從哪里開始找起?

一般來說從你的樹中保留的第一個對象開始找起是個好辦法,因?yàn)楸槐A舻膬?nèi)容是按照距離排序的(也就是到 window 的距離)。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_37.jpg" alt="image" />

一般來說,保留的對象中,有著最短距離的通常是最有可能造成內(nèi)存泄漏的。

Q:總結(jié),比較,主導(dǎo)和包含視圖都有哪些不同?

屏幕的底端可以選擇不同的數(shù)據(jù)視圖以實(shí)現(xiàn)不同的作用。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_38.jpg" alt="image" />

  • 總結(jié)視圖可以幫助你在基于構(gòu)造器名稱分組的狀態(tài)下尋找對象(它們的內(nèi)存使用狀況)。這個視圖對于追蹤 DOM 泄漏非常有用。
  • 比較視圖通過顯示對象是否被垃圾回收器清理了來幫助你追蹤內(nèi)存泄露。一般用于記錄并比較某個操作前后的兩個(或更多)內(nèi)存快照。具體的做法就是,檢查釋放內(nèi)存以及引用計(jì)數(shù)的增量來讓你確認(rèn)內(nèi)存泄露是否存在并找出其原因。
  • 包含視圖提供了關(guān)于對象結(jié)構(gòu)的一個良好的視角,讓我們可以分析在全局命名空間(比如 window)下的對象引用情況,以此來找出是什么讓它們保留下來了。這樣就可以從比較低的層次來分析閉包并深入對象內(nèi)部。
  • 主導(dǎo)視圖幫助我們確認(rèn)是否有意料外的對象引用依舊存在(它們應(yīng)該是有序地包含著的)以及垃圾回收確實(shí)處于運(yùn)行狀態(tài)。

Q:在堆分析器中不同的構(gòu)建器入口對應(yīng)什么功能?

  • (global property) - 在全局對象(就像是 window)和其引用的對象之間的中間對象。如果一個對象是用名為 Person 的構(gòu)造器創(chuàng)建的并且被一個全局對象持有,那么保留路徑看起來就是這樣的:[global] > (global property) > Person。這和對象直接引用其他對象的情況相反,但是我們引入中間對象是有著原因的。全局對象會周期性修改并且對于非全局對象訪問的優(yōu)化是個好方法,并且這個優(yōu)化不會對全局對象生效。
  • (roots) - 保留樹視圖中的根節(jié)點(diǎn)入口是指含有對選中對象的引用的入口。這些也可以是引擎處于其自身目的而創(chuàng)建的。引擎緩存了引用對象,但是這些引用全部都是弱類型的,因此它們不會阻止其他對象被回收。
  • (closure) - 通過函數(shù)閉包引用的一組對象的總數(shù)。
  • (array,string,number,regexp) - 引用了數(shù)組,字符串,數(shù)字或者常規(guī)表達(dá)式的對象屬性列表。
  • (compiled code) - 簡單點(diǎn)說,所有事情都和編譯后的代碼相關(guān)。腳本類似于一個函數(shù)但是要和 <script> 標(biāo)簽對應(yīng)。SharedFunctionInfos(SFI)是在函數(shù)和編譯后的代碼之間的對象。函數(shù)通常會有上下文,而 SFI 則沒有。
  • HTMLDivElement,HTMLAnchorElement,DocumentFragment - 被你的代碼引用的特定類型的元素或者文檔對象的引用。

其他的很多對象在你看來就像是在你代碼的生存期內(nèi)產(chǎn)生的,這些對象可能包含了事件監(jiān)聽器以及特定對象,就像是下面這樣:

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_40.jpg" alt="image" />

Q:在 Chrome 中為了不影響到我的圖表有什么功能是應(yīng)該關(guān)閉的嗎?

在 Chrome DevTools 中使用設(shè)置的時(shí)候,推薦在化名模式下并關(guān)閉所有擴(kuò)展功能或者直接通過特定用戶數(shù)據(jù)目錄來啟動 Chrome(--user-data-dir="")。

http://wiki.jikexueyuan.com/project/chrome-devtools/images/image_41.jpg" alt="image" />

如果希望圖表盡可能的精確的話,那么應(yīng)用,擴(kuò)展插件甚至是控制臺日志都可能隱式地影響到你的圖表。

結(jié)束語

今天的 JavaScript 引擎在多種情況下都可以自動清理代碼中產(chǎn)生的垃圾。也就是說,它們只能做到這里了,而我們的代碼中仍然會由于邏輯問題出現(xiàn)內(nèi)存泄露。請運(yùn)用這些工具來找出你的瓶頸,并記住,不要去猜測它,而是去測試。