記一次網頁內存溢出分析及解決實踐

背景

項目是利用vue框架開發的公司內部的異常監控系統,用於顯示java程序運行時的異常信息,包括執行堆棧、代碼、變量等信息顯示。vue

在測試過程當中,部門同事反映:在不一樣的異常信息之間屢次切換,會致使網頁崩潰。在案發現場打開 chrome 的任務管理器,看到這個頁面內存佔用已經達到了9.7G,初步懷疑頁面存在泄漏。java

驗證猜想

  1. 打開 devtool -> performance,開始記錄頁面性能
  2. 執行頁面上切換其它異常信息的操做(頁面最有可能引發內存泄漏的操做)
  3. 查看性能分析結果

能夠看到Nodes、Listeners、JS Head(memory) 的階梯式增加,中間的增加節點對應就是每一次操做。很顯然這個操做會引發內存的持續增加,最終發生內存溢出也是瓜熟蒂落了。web

問題分析

在動手以前,我已知的信息有:chrome

  1. 從performace工具,能夠看到 JS Heap、Nodes、Listeners 的累積增加
  2. 以上三點,實際上存在依賴關係:
  • 變量引用DOM
  • 子級DOM不能釋放,會致使父級也不會被釋放
  • 若是DOM能被正常GC, 對DOM的事件監聽器也會自動移除
  1. 可使用 devtool->memory -> take snapshot 收集內存快照並分析工具文檔;

簡單認識一下 snapshot

嗯,內容有點多,可是也還算清晰:api

  1. 按數據類型進行統計,能夠看到一些內建對象、 Vue 對象、自定義對象(好比 Exception、StackFrame 等)、Detached Element、EventListener等等。
  2. 縱座標有Distance, shallow size, Retained Size, 能夠不許確理解爲:
  • Distance:到root的引用距離
  • Shallow size:對象自己的大小,不包含它引用的數據的大小
  • Retained size:對象自身以及全部引用的大小,就是對象總共佔用的內存 (若是它引用的對象不被其餘不可回收的對象引用的時候。用google開發者網站的描述叫:將對象自己連同其沒法從GC根到達的相關對象一塊兒刪除後釋放的內存大小)
  1. 下面的 retainers 面板,能夠看到變量的具體引用路徑、在哪裏被建立、以及在哪裏被使用

定位問題:找到那些被引用本該被釋放,但實際沒有的釋放的對象

  1. 執行引發內存泄漏的操做

該操做的核心代碼大體是這樣的 架構


主要功能是,每次執行setEvent,都將 this.exception 指向新的實例,並交給頁面進行數據展現,而以前被this.exception引用的對象,應該被釋放。

  1. 從新收集新的內存快照信息框架

  2. 找出差別:將視圖改成差別視圖chrome-devtools

從圖上能夠看到在步驟1以後,出現了不少新增的對象,可是刪除的對象是0。

以 Exception對象爲例,按照步驟1的代碼邏輯,新對象創建,舊對象被釋放,Delta 應該爲零。因此能夠明確知道,這裏是一個問題。不過這裏點開查看變量的引用詳情,並沒獲得太多有用的信息,只知道被哪一個 vue component 引用了,可是component 太多,不太好定位。函數

查看 Listenters, 我看到的畫風基本是這樣的:工具

跟預期的結果一致,都是因爲一些 Nodes 沒有被釋放致使的。不過確實沒有獲得太多方便分析的信息。

另外查看Nodes相關的信息,搜索 Detached, 能夠看到一些 Detatched HTMLDivElement等等相似的對象,也就是在內存中可是沒有在頁面進行渲染的元素

我找了一個detla比較小的、節點功能也清晰(就是用來在頁面中進行代碼高亮的元素)的 Detatched HTMLPreElement 進行分析:

能夠看到實際引用關係爲 div <= div <= div <= vue component <= var-hover <= events <= ... $platform.event...

在這裏 $platform.event 是由平臺 + 模塊的架構設計中,平臺提供的事件 api, 用於全局的事件通訊。

最終將以上引用關係進行翻譯:由平臺提供的事件 $platform.event (全局,綁定的事件函數不會被自動釋放),綁定了一個叫作 var-hover 的事件 => var-hover 的事件函數中引用了一個 vue-component => component 的$el屬性 引用了某個Dom => Dom的父級被子級引用致使不能被GC。

能夠看看 var-hover 的代碼:


var-hover 綁定了一個匿名函數(基本上也能夠知道,沒有給這個事件沒有寫過解綁操做),而後匿名函數中使用了 this, 也就是當前 vue component,這也致使了被這個 component 引用的對象都不能被GC。

因此禍根基本上找到了,接下來要作的就是:修復 -> 從新驗證

修復

  1. 第一次簡單修改:在 beforeDestroy 中進行事件解綁,當時驗證確認內存溢出問題已解決
  2. 手動解綁這是個大坑,不少地方不少人在編寫代碼的時候,真不必定有這個好習慣。全部也就有了如今的處理方案:對平臺接口進行改造,支持事件的基於組件的自動解綁。代碼以下:

這就是$platform.event 的實際實現

var-hover的事件綁定以下


移除了 beforeDestroy 鉤子,業務層看起來也好多了。

驗證

  1. 利用 performance 功能,屢次進行以前致使內存溢出的操做,獲得結果以下

這裏的每次峯值,就是剛執行進行操做時進行內存分配的結果,以後每次執行,並無出現內存及 Nodes, Listensers 的累積

再次對比一下修正以前的性能分析結果


可怕的樓梯。。。

  1. 順便再 memory 面板中出現了什麼變化

多了一個 StackFrameVar 以及一些爲了呈現這個 StackFrameVar 對象多出來的一些EventListener、Observer等,這是因爲兩次呈現的數據自己不同致使的,屬於正常狀況

Exception、 等不少對象的 Delta 已經爲0了(按 Delta倒序排列的)

其它說明

以上分析圖是寫這篇文章過程當中,回寫部分代碼以後實時分析的,相對而言沒有實際調試時處理得那麼細緻。實際調試過程當中還作了其它操做:

  1. 隱私窗口,禁用全部擴展(避免影響內存分析)
  2. 關閉開發模式HMR功能,由於 VUE_HOT_RELOAD 也會產生一層引用,我並不能徹底信任它
  3. 使用模擬數據,每次執行操做,都會渲染同樣的可被人工計算清楚(知道哪一個類會產生多少實例)的數據
  4. performance 過程當中手動GC

經過以上方式是爲了提供一個徹底純淨可控的分析環境。

相關文章
相關標籤/搜索