在構(gòu)建高性能、長(zhǎng)周期運(yùn)行的 WebGL/Canvas 應(yīng)用(如 3D 編輯器、數(shù)據(jù)可視化平臺(tái))時(shí),內(nèi)存管理是一個(gè)至關(guān)重要且極具挑戰(zhàn)性的課題。
開發(fā)者通常面臨的內(nèi)存泄漏問(wèn)題,其根源遠(yuǎn)比簡(jiǎn)單的 JavaScript 對(duì)象未釋放要復(fù)雜得多。一個(gè)現(xiàn)代 WebGL/Canvas 應(yīng)用的內(nèi)存版圖實(shí)際上跨越了三個(gè)截然不同但又相互關(guān)聯(lián)的內(nèi)存區(qū)域:
圖 V8 引擎管理的 JavaScript 堆(JS Heap),絕大部分情況最關(guān)注的是這一層的泄露
Blink 渲染引擎自身用于管理 DOM 等對(duì)象的原生 C++ 堆(Native Heap)
這三個(gè)內(nèi)存區(qū)域各自遵循不同的分配、管理和回收規(guī)則:
V8 堆:采用先進(jìn)的、自動(dòng)化的垃圾回收(GC)機(jī)制,當(dāng)引用為空的時(shí)候會(huì)自動(dòng)釋放
GPU 顯存:依賴于開發(fā)者通過(guò) WebGL API 進(jìn)行顯式的手動(dòng)管理
Blink 的原生堆:由專用 C++ 垃圾回收器負(fù)責(zé)
大多數(shù)難以診斷和修復(fù)的內(nèi)存泄漏問(wèn)題,其本質(zhì)都源于對(duì)這三個(gè)層面之間的邊界、所有權(quán)規(guī)則以及通信協(xié)議缺乏深刻理解。
我們將分別對(duì)三個(gè)核心部分,系統(tǒng)性地分析每一層內(nèi)存區(qū)域中常見的泄漏模式、底層成因,并介紹實(shí)用排查策略和解決方案。通過(guò)本篇分享,開發(fā)者將能夠建立一個(gè)貫穿 GPU、JavaScript 引擎和瀏覽器渲染內(nèi)核的整體內(nèi)存心智模型,從而更有效地構(gòu)建穩(wěn)定、高效且無(wú)泄漏的 WebGL/Canvas 應(yīng)用。
JavaScript 堆泄漏
堆簡(jiǎn)述
Javascript 的解釋器 V8 引擎將瀏覽器內(nèi)存分為兩個(gè)主要部分:
■棧(Stack):用于存儲(chǔ)靜態(tài)數(shù)據(jù),包括原始類型(Primitive Types)的局部變量(如 number, boolean, null, undefined, string 等)以及指向堆中對(duì)象的指針(引用地址)。棧內(nèi)存的特點(diǎn)是大小固定、自動(dòng)分配和釋放,隨著函數(shù)調(diào)用的開始和結(jié)束(執(zhí)行上下文的入棧和出棧)而進(jìn)行管理。
■堆(Heap):用于存儲(chǔ)動(dòng)態(tài)分配的內(nèi)存,即大小不固定的、生命周期可能很長(zhǎng)的數(shù)據(jù)。JavaScript 中的絕大多數(shù)“東西”都存在這里。譬如對(duì)象(object),數(shù)組(Arrays),函數(shù)(Functions),閉包(Clousures),字符串(String),ArrayBuffer / Uint8Array 等。
我們重點(diǎn)關(guān)注堆上的資源,盡管有自動(dòng)垃圾回收(GC)機(jī)制,JS 堆泄漏仍然是 WebGL 應(yīng)用中一個(gè)常見且棘手的問(wèn)題。
導(dǎo)致 JS 內(nèi)存泄漏的常見架構(gòu)模式:
在 JavaScript 這類具備自動(dòng)垃圾回收機(jī)制的語(yǔ)言中,內(nèi)存泄漏的本質(zhì)并非“忘記釋放內(nèi)存”,而是存在“意外的引用”(unwanted reference)。一個(gè)在邏輯上已經(jīng)廢棄、應(yīng)用不再需要的對(duì)象,若仍有一條引用鏈將其與存活的對(duì)象圖相連,GC 就會(huì)判定它“可達(dá)”,從而無(wú)法回收 。
GC 標(biāo)記 - 清除(Mark-and-Sweep)算法遵循明確的規(guī)則:從根對(duì)象開始,逐條追蹤所有指針。只要從根到某個(gè)對(duì)象存在一條路徑,該對(duì)象就定義為可達(dá),即“存活”。但是 GC 無(wú)法理解開發(fā)者的語(yǔ)義意圖 —— 它無(wú)法判斷一個(gè)已經(jīng)脫離文檔的 DOM 節(jié)點(diǎn)是否永遠(yuǎn)不會(huì)被重新掛載,也無(wú)法知曉閉包中捕獲的變量是否永遠(yuǎn)不會(huì)被訪問(wèn)。它只能機(jī)械性地遵循指針。
因此,修復(fù)這些內(nèi)存泄漏并非尋找 V8 引擎的 bug,而是細(xì)致地管理對(duì)象圖,確保當(dāng)對(duì)象在邏輯上不再需要時(shí),通過(guò) myNode = null 或 removeEventListener 等方式顯式地切斷引用。
以下是一些常見的導(dǎo)致意外引用的模式:
1、 分離的 DOM 元素
分離的 DOM 元素 (Detached DOM Elements) 這是最經(jīng)典的泄漏模式之一。當(dāng)一個(gè) DOM 節(jié)點(diǎn)通過(guò) element.removeChild() 從文檔樹中移除后,它在頁(yè)面上就不再可見。但是,如果此時(shí) JavaScript 代碼中仍有某個(gè)變量持有對(duì)該節(jié)點(diǎn)的引用,那么這個(gè)節(jié)點(diǎn)及其整個(gè)子樹都無(wú)法被 GC 回收。在復(fù)雜的單頁(yè)應(yīng)用(SPA)中,視圖組件被動(dòng)態(tài)創(chuàng)建和銷毀,如果銷毀邏輯不完善,很容易留下對(duì)舊視圖 DOM 節(jié)點(diǎn)的引用。
2、閉包引起的意外作用域捕獲
閉包(Closure)是 JavaScript 的一個(gè)強(qiáng)大特性,它允許函數(shù)訪問(wèn)并操作其詞法作用域(lexical scope)中的變量,即使該函數(shù)已在其作用域之外被調(diào)用。但是這份強(qiáng)大也暗藏風(fēng)險(xiǎn),閉包往往是內(nèi)存泄漏的“隱形源頭”。
閉包會(huì)完整持有其創(chuàng)建時(shí)所在作用域的引用。若一個(gè)生命周期很長(zhǎng)的內(nèi)部函數(shù)(例如,一個(gè)事件回調(diào)或定時(shí)器回調(diào))是在一個(gè)包含大型對(duì)象引用的外部函數(shù)中創(chuàng)建的,那么這個(gè)大型對(duì)象也會(huì)被閉包“捕獲”。即使內(nèi)部函數(shù)本身從未使用過(guò)它,大型對(duì)象也會(huì)始終處于可達(dá)狀態(tài),最終導(dǎo)致垃圾回收器無(wú)法對(duì)其回收,從而造成內(nèi)存泄漏。
3、 懸空的定時(shí)器和事件監(jiān)聽器
傳遞給 setInterval、setTimeout 或 element.addEventListener 的回調(diào)函數(shù),其生命周期會(huì)持續(xù)到定時(shí)器被清除或事件監(jiān)聽器被移除為止。在此期間,若回調(diào)函數(shù)內(nèi)部引用了其他對(duì)象(比如某個(gè)組件的實(shí)例或數(shù)據(jù)),這些被引用的對(duì)象也會(huì)被“綁定”而保持存活狀態(tài)。在組件化開發(fā)中,最常見的疏漏之一便是:在組件銷毀時(shí)忘記清理這些定時(shí)器與事件監(jiān)聽器。這就會(huì)直接導(dǎo)致整個(gè)組件實(shí)例及其依賴對(duì)象始終處于可達(dá)狀態(tài),最終無(wú)法被回收,從而造成內(nèi)存泄漏。
4、意外的全局變量
在非嚴(yán)格模式下,函數(shù)內(nèi)給未聲明的變量進(jìn)行賦值,JavaScript 不會(huì)報(bào)錯(cuò),反而會(huì)在全局對(duì)象(如瀏覽器的 window)上創(chuàng)建一個(gè)同名變量。全局變量作為 GC 根節(jié)點(diǎn),它們?cè)趹?yīng)用的整個(gè)生命周期內(nèi)都無(wú)法被回收。這種“意外的全局變量”通常由拼寫錯(cuò)誤或忘記使用 let、const、var 關(guān)鍵字引起,是一種隱蔽但危害嚴(yán)重的內(nèi)存泄漏源。
Chrome DevTools 堆分析實(shí)戰(zhàn)指南
Chrome DevTools(開發(fā)者工具) 的 Memory(內(nèi)存)面板是診斷 JS 堆泄漏的權(quán)威工具。它可以對(duì)當(dāng)前堆進(jìn)行快照,直觀的展示當(dāng)前的占用情況。
具體操作位置在 Chrome DevTools -> Momery 標(biāo)簽頁(yè)(圖中 ①)。
1.在內(nèi)存分析中,Heap snapshot(堆快照)是最常用的排查手段,在生成快照前,需先選擇這一類型(圖中 ②)。
2.在生成快照前,需要先點(diǎn)擊上圖中的 ③ 號(hào)按鈕(強(qiáng)制垃圾回收),待完成一次 GC 后,再點(diǎn) ④ 號(hào)按鈕生成快照。這樣做的原因是,我們的核心目標(biāo)是排查內(nèi)存泄漏問(wèn)題,強(qiáng)制 GC 能釋放原本應(yīng)該被回收的資源,這會(huì)讓快照結(jié)果更加直觀地顯示出問(wèn)題。
3.快照生成后,在 ⑤ 位置會(huì)顯示快照信息,展開后如下:
(⑥ 位置會(huì)展示堆內(nèi)存的大小,能快速且直觀地了解到整個(gè)頁(yè)面的堆內(nèi)存占用情況。)
在快照信息中,需要重點(diǎn)關(guān)注每個(gè)對(duì)象的兩項(xiàng)核心數(shù)據(jù):
■Shallow Size 淺層大?。▓D中 ①):一般用來(lái)指對(duì)象自身占用的大小,不包含它引用的其他對(duì)象的大小。
■Retained Size 保留大小(圖中 ②):表示該對(duì)象在被 GC 后,所能釋放的總內(nèi)存大小。通常等于自身的 Shallow Size 加上被它引用的其他對(duì)象的 Shallow Size 之和。
在實(shí)際分析中,建議優(yōu)先關(guān)注 Retained Size,因其能更全面地反映對(duì)象堆內(nèi)存占用的實(shí)際影響。
快照的摘要視圖
在上圖所示的摘要中,每一項(xiàng)都支持展開,展開后可以看到對(duì)象的完整引用鏈。摘要面板適合的運(yùn)用場(chǎng)景:當(dāng)單次 Profile 已顯示出大量的內(nèi)存占用時(shí),可先按 Retained Size 對(duì)列表進(jìn)行排序,快速定位到占據(jù)了過(guò)高的內(nèi)存的項(xiàng),展開其中的可疑目標(biāo)并一路追溯,直到找到根源 —— 通常是掛載到全局 windows 對(duì)象上的變量,或被閉包捕獲的變量。
三快照法(推薦的排查步驟)
在多數(shù)情況下,泄露是緩慢發(fā)生的,單個(gè)堆快照包含了數(shù)百萬(wàn)個(gè)對(duì)象,雜亂無(wú)章,不方便直接找到泄漏源。因此,我們更推薦使用“三快照法”來(lái)找到泄露的源頭。具體操作步驟:
1.快照 1 (基線狀態(tài)):加載頁(yè)面,在應(yīng)用進(jìn)入穩(wěn)定狀態(tài)后,點(diǎn)快照中的掃把按鈕,做一次強(qiáng)制 GC 后,拍攝第一次堆快照(Heap snapshot),建立內(nèi)存的基線
2.執(zhí)行可疑操作:執(zhí)行一系列你懷疑可能導(dǎo)致內(nèi)存泄漏的用戶操作。這里的關(guān)鍵在于:這個(gè)操作序列應(yīng)具備是可逆性。例如“打開一個(gè)復(fù)雜的 UI 面板,隨后再將其關(guān)閉”。這個(gè)“操作-逆操作”循環(huán)是你的受控實(shí)驗(yàn),假設(shè)是“該循環(huán)應(yīng)是內(nèi)存中性的,即操作后不應(yīng)遺留任何內(nèi)存垃圾”。此外,也可測(cè)試應(yīng)用長(zhǎng)時(shí)間靜置(如半小時(shí)以上)的情況。
3.快照 2:做完上述的操作之后,繼續(xù)強(qiáng)制 GC 一次,再拍攝第二次快照。
4.放大泄漏:重復(fù)執(zhí)行步驟 2 中的“操作-逆操作”循環(huán)數(shù)次(例如 1-N 次)。這會(huì)放大內(nèi)存泄漏,使其在快照對(duì)比中更加明顯。
5.快照 3:完成所有循環(huán)后,再次強(qiáng)制 GC,并拍攝第三次快照。
1、使用對(duì)比視圖
在完成以上的操作步驟后,選擇第三個(gè)快照,并在頂部的視圖選擇器中(下圖 ②),將視圖模式從 Summary 切換為 Comparison,比較對(duì)象選擇為快照 2(下圖 ③)?,F(xiàn)在視圖只會(huì)顯示快照 2 和快照 3 之間發(fā)生變化的對(duì)象。操作后需要關(guān)注以下內(nèi)容:
■Delta 列:這是該視圖的核心,它顯示了對(duì)象實(shí)例數(shù)量的凈變化。需重點(diǎn)關(guān)注 Delta 值為正數(shù)的項(xiàng),尤其是那些與重復(fù)操作次數(shù)成正比的構(gòu)造函數(shù)。這些就是在操作循環(huán)中被創(chuàng)建但未能被成功回收的對(duì)象。
■Retained Size Delta 列:此列顯示了該類對(duì)象及其引用的所有對(duì)象所占內(nèi)存的凈增量。按此列降序排序,可以快速定位到對(duì)內(nèi)存影響最大的泄漏源。
2、使用摘要視圖
還有一種很重要的排查方式:
■選擇第三個(gè)快照,頂部的視圖選擇器,切換為 Summary
■右側(cè)下拉框中選擇篩選快照一和快照二中間創(chuàng)建的對(duì)象
該視圖的意圖是:查找出快照 2 較快照 1 新增的內(nèi)存對(duì)象,若這些新增對(duì)象在快照 3 中依然存在,那么它們極有可能是泄露的源頭。
3、使用 Retainers 樹追溯泄漏源
在對(duì)比視圖中定位到一個(gè)可疑的泄漏對(duì)象(即對(duì)應(yīng)的構(gòu)造函數(shù))后,展開該構(gòu)造函數(shù),并選中其中一個(gè)實(shí)例。此時(shí),下方的 Retainers(保留者)面板會(huì)自動(dòng)加載內(nèi)容。這個(gè)面板是定位內(nèi)存泄漏根源的核心工具,面板展示了一條或多條引用鏈,并清晰地解釋了被選中對(duì)象無(wú)法被 GC 回收的原因。
具體分析步驟如下:
■追溯引用鏈:Retainers 樹以被選中的對(duì)象為起點(diǎn),逐層向上追溯,直到指向某個(gè) GC 根節(jié)點(diǎn)(例如 (Global handles) 下的 window 對(duì)象)。開發(fā)者需要仔細(xì)檢查這條鏈路上的每個(gè)節(jié)點(diǎn)。
■識(shí)別意外引用:尋找那些本應(yīng)在操作結(jié)束后被切斷的引用。例如,一個(gè)已關(guān)閉面板的 DOM 節(jié)點(diǎn),仍被一個(gè)全局緩存對(duì)象 myApp.cache 引用,那么 myApp.cache 就是那個(gè)“意外的引用”。
■關(guān)注高亮節(jié)點(diǎn):分析分離的 DOM 樹時(shí),DevTools 會(huì)用顏色高亮節(jié)點(diǎn)。
黃色節(jié)點(diǎn): 表示被 JavaScript 代碼直接引用的節(jié)點(diǎn)。
紅色節(jié)點(diǎn):表示無(wú)直接引用,但因?qū)儆谀硞€(gè)黃色節(jié)點(diǎn)的父子節(jié)點(diǎn),而被間接保留在內(nèi)存中的節(jié)點(diǎn)。在排查時(shí),應(yīng)優(yōu)先關(guān)注黃色節(jié)點(diǎn)。
GPU 顯存與 WebGL 上下文管理
本部分內(nèi)容將聚焦于 GPU 中的關(guān)鍵資源,此類資源必須通過(guò) WebGL API 進(jìn)行顯式的、手動(dòng)的生命周期管理。這背后的核心邏輯在于:在 GPU 層面不存在自動(dòng)內(nèi)存管理機(jī)制。從資源的創(chuàng)建、綁定到最終銷毀,開發(fā)者須全程主導(dǎo),主動(dòng)承擔(dān)釋放內(nèi)存的全部責(zé)任。
WebGL 上下文句柄
WebGL 上下文句柄是一種有限且關(guān)鍵的資源。現(xiàn)代瀏覽器對(duì)單個(gè)頁(yè)面或同源(origin)下可創(chuàng)建的活動(dòng) WebGL 上下文(Context)數(shù)量施加了嚴(yán)格的限制。例如,在 Chrome 瀏覽器中,這個(gè)上限通常是 16 個(gè)。Firefox 也有類似的限制,盡管具體數(shù)值和配置策略可能略有不同。
這個(gè)限制是瀏覽器廠商為保護(hù)整個(gè)系統(tǒng)穩(wěn)定性而采取的一項(xiàng)關(guān)鍵防御措施。GPU 是一種系統(tǒng)級(jí)的共享資源,如果單個(gè)網(wǎng)頁(yè)能夠無(wú)限制地創(chuàng)建 WebGL 上下文,它將可能耗盡 GPU 驅(qū)動(dòng)程序的資源,導(dǎo)致驅(qū)動(dòng)崩潰或整個(gè)操作系統(tǒng)的性能下降,從而影響到其他應(yīng)用程序和系統(tǒng)界面的正常運(yùn)行。
我們會(huì)經(jīng)常看到,作為系統(tǒng)級(jí)資源管理者的瀏覽器,其抉擇始終是:優(yōu)先保障宿主操作系統(tǒng)的穩(wěn)定性,而非滿足單個(gè)網(wǎng)頁(yè)的無(wú)節(jié)制資源需求。
當(dāng) WebGL Context 超出限制,瀏覽器會(huì)采取強(qiáng)制措施:丟棄“最近最少使用”的那個(gè) WebGL 上下文,并在控制臺(tái)輸出一條警告,如:“WARNING: Too many active WebGL contexts. Oldest context will be lost.”(警告:活動(dòng) WebGL 上下文過(guò)多。最舊的上下文將被丟棄。)。對(duì)于那些未預(yù)料到此行為的應(yīng)用而言,這可能導(dǎo)致災(zāi)難性的渲染失敗,且問(wèn)題難以追蹤。
對(duì)于確實(shí)需要大量獨(dú)立 3D 視圖的應(yīng)用(例如建筑設(shè)計(jì)軟件、多視圖監(jiān)控面板),必須采用更高級(jí)的架構(gòu)模式來(lái)規(guī)避此限制。常見的解決方案推薦復(fù)用 gl context,切換場(chǎng)景的時(shí)候,做 clear + dispose 操作清空,并使用同一個(gè) g3d 進(jìn)行反序列化。
貼圖,buffer 等 GPU 資源對(duì)象
在 WebGL 環(huán)境中,代表 GPU 資源的 JavaScript 對(duì)象(例如 WebGLTexture 對(duì)象),其生命周期與該資源在 GPU 顯存中實(shí)際占用的內(nèi)存的生命周期是完全分離的。簡(jiǎn)單地將 JavaScript 對(duì)象的引用設(shè)置為 null,或讓其離開作用域而被垃圾回收,也不會(huì)觸發(fā) GPU 顯存的釋放。
WebGL API 劃定了一條清晰的界線:JavaScript 的 WebGLTexture 對(duì)象僅僅是一個(gè)輕量級(jí)的句柄(handle),本質(zhì)上是一個(gè)整數(shù) ID。JS GC 可以安全地回收這個(gè)句柄對(duì)象,且不會(huì)對(duì) GPU 產(chǎn)生任何影響。而真正占用顯存(VRAM)的重量級(jí) GPU 資源,唯有開發(fā)者——這個(gè)唯一掌握渲染邏輯上下文的角色——顯式調(diào)用對(duì)應(yīng)的刪除函數(shù)時(shí),才會(huì)被徹底釋放。因此,一旦某個(gè) GPU 資源不再需要,就必須立即調(diào)用對(duì)應(yīng)的刪除函數(shù),例如:
gl.deleteTexture()
gl.deleteBuffer()
gl.deleteRenderbuffer()
gl.deleteFramebuffer()
gl.deleteProgram()
gl.deleteShader()
一個(gè)標(biāo)準(zhǔn)的 WebGL 資源生命周期應(yīng)遵循“創(chuàng)建-綁定-使用-解綁-刪除”的模式。GPU 顯存泄漏并非瀏覽器的“缺陷”,而是開發(fā)者未能遵守這一顯式契約的結(jié)果。
HT 中的 graph3dView 提供了專門的 dispose 方法,當(dāng) 3D 場(chǎng)景確定要釋放的時(shí)候,主動(dòng)調(diào)用 g3d.dispose() 將會(huì)徹底把當(dāng)前的所有跟 WebGL 相關(guān)的 GPU 資源徹底釋放。
查看這類資源占用,通常需要觀察系統(tǒng)顯卡的顯存使用情況。以 Windows 系統(tǒng)為例,可以通過(guò):「任務(wù)管理器 → 性能 → GPU → 專用 GPU 內(nèi)存」這一路徑,直觀地看到顯存占用的變化趨勢(shì)。以一個(gè) 6G 顯存的 GPU 為例,盡量將顯存占用控制在合理范圍(譬如 5G 以內(nèi),避免超過(guò) 5.5G),否則一旦超標(biāo),系統(tǒng)可能會(huì)強(qiáng)制回收顯存資源。
原生堆:理解 Blink 的 Oilpan GC
分析完應(yīng)用層的內(nèi)存問(wèn)題,我們的視線將最終聚焦于瀏覽器的 C++ 底層核心 ——Blink 渲染引擎。
Blink Oilpan GC 是 Chromium 瀏覽器引擎 Blink 中用于管理 C++ 對(duì)象內(nèi)存的垃圾回收 (Garbage Collection, GC) 系統(tǒng)。Oilpan 采用的是一種先進(jìn)的并發(fā)標(biāo)記與增-量清除 (Concurrent Marking and Incremental Sweeping) 垃圾回收機(jī)制。這種機(jī)制的核心思想是盡可能地將垃圾回收的工作與主線程 (main thread) 的任務(wù)(例如 JavaScript 執(zhí)行、頁(yè)面布局和渲染)并行處理,從而最大限度地減少因 GC 而導(dǎo)致的頁(yè)面卡頓 (jank)。
通常這塊內(nèi)存由 Blink 底層管理,Web 應(yīng)用層是無(wú)法干預(yù)的,這里我們通過(guò)一個(gè)實(shí)際案例來(lái)展開說(shuō)明:JS 堆快照顯示其 24 小時(shí)動(dòng)畫運(yùn)行后內(nèi)存增長(zhǎng)微乎其微,但 Windows 資源監(jiān)視器卻顯示 Chrome 進(jìn)程占用了 4GB 內(nèi)存。這種懸殊的差距,讓人不禁好奇。
■在 Chrome 地址欄輸入 chrome://tracing 并訪問(wèn)
■點(diǎn)擊頁(yè)面中的 "Record" 按鈕,進(jìn)入錄制配置界面
■選擇 "Manually select settings" 選項(xiàng)
■點(diǎn)擊 "Edit categories" 按鈕,打開配置列表
■在彈出的類別列表中,務(wù)必勾選 memory-infra。
■點(diǎn)擊 "OK" 確認(rèn)配置后,再次點(diǎn)擊 "Record" 開始錄制。等待一段時(shí)間后點(diǎn)擊結(jié)束
■錄制結(jié)束后,點(diǎn)擊鍵盤的 M 鍵查看具體的內(nèi)存快照
從上圖可見,blink_gc 占用 4GB 內(nèi)存,這可能并非內(nèi)存泄漏,而是 Blink 的 Oilpan GC 策略導(dǎo)致的正?,F(xiàn)象。其核心機(jī)制是內(nèi)存池化 (Memory Pooling):Blink 會(huì)預(yù)先向操作系統(tǒng)申請(qǐng)大塊的連續(xù)內(nèi)存區(qū)域。頁(yè)面中幾乎所有的 Blink C++ 對(duì)象(包括大量臨時(shí)的字符串、數(shù)組等)都在這個(gè)大內(nèi)存池中進(jìn)行分配,以提升效率。
當(dāng)這些短生命周期的對(duì)象不再被引用時(shí),它們?cè)谶壿嬌媳灰暈椤袄保?strong>但它們所占用的物理內(nèi)存并不會(huì)立即歸還給操作系統(tǒng)。GC 回收器會(huì)根據(jù)當(dāng)前的內(nèi)存壓力 (Memory Pressure) 來(lái)決定何時(shí)執(zhí)行徹底的清理。
在本案例中,機(jī)器總內(nèi)存高達(dá) 64GB,資源充裕,Chrome 判斷無(wú)需迫切回收。為避免不必要的性能開銷(一次完整的 GC 會(huì)消耗 CPU 資源),GC 選擇推遲回收操作。因此,我們看到的 4GB 占用,實(shí)際上是 Oilpan GC 持有的一個(gè)較大的內(nèi)存池,其中包含了活動(dòng)對(duì)象和大量待回收的“垃圾”對(duì)象。只要這個(gè)內(nèi)存池的大小趨于穩(wěn)定,沒(méi)有出現(xiàn)持續(xù)、無(wú)節(jié)制的增長(zhǎng),通常就不構(gòu)成內(nèi)存泄漏問(wèn)題。
HT 與內(nèi)存泄露
綜合上述的三部分內(nèi)容,我們捋清了內(nèi)存泄漏問(wèn)題的主要原因,并掌握了對(duì)應(yīng)的排查方法。而在 HT 框架中,內(nèi)存泄漏的問(wèn)題在 3D 場(chǎng)景中最為常見,由于 HT 的 3D 是基于 WebGL 實(shí)現(xiàn)的,此類泄漏往往會(huì)表現(xiàn)得尤為明顯。
為了清晰呈現(xiàn) HT 3D 中的內(nèi)存泄漏問(wèn)題,我們?cè)O(shè)計(jì)了一個(gè)簡(jiǎn)單的對(duì)照實(shí)現(xiàn)來(lái)進(jìn)行演示。
對(duì)照實(shí)驗(yàn)
在展開實(shí)驗(yàn)前,我們先簡(jiǎn)要了解下 HT 框架的核心架構(gòu)。HT 采用 MV 架構(gòu)模式,在 HT 的框架設(shè)計(jì)中, Data 模型和 View 視圖是分離的,二者之間通過(guò) Event 事件監(jiān)聽和派發(fā)機(jī)制來(lái)建立起數(shù)據(jù)綁定。
在實(shí)驗(yàn)操作前,我們可以打開 Chrome DevTools -> Performance (性能) 面板,并且點(diǎn)擊面板中的錄制按鈕,記錄整個(gè)實(shí)驗(yàn)過(guò)程,這能幫助我們?cè)诓僮鹘Y(jié)束后,回溯并分析全程性能和內(nèi)存的變化情況。
■實(shí)驗(yàn)環(huán)境:
瀏覽器:Chrome 138.0.7204.101(64位)
顯卡:NVIDIA GeForce GTX 1660 Ti
處理器:Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz (2.90 GHz)
第一次實(shí)驗(yàn)
我們通過(guò)按鈕不斷創(chuàng)建新的視圖,當(dāng)頁(yè)面中超過(guò)一定數(shù)量Graph3dView 時(shí),可以看到第一個(gè)場(chǎng)景“崩潰”,但是當(dāng)我們刪除最后一個(gè) Graph3dView 后,第一個(gè)場(chǎng)景又恢復(fù)了。
我們可以從 Performance 面板中觀察到整個(gè)過(guò)程:
■當(dāng)首個(gè) WebGL 上下文被銷毀后,JS 堆內(nèi)存出現(xiàn)明顯下降。
■刪除最新的視圖后,首個(gè) WebGL 恢復(fù),且刪除后事件監(jiān)聽器占用的內(nèi)存下降
由于 HT 是 MV 框架,雖然瀏覽器銷毀了 WebGL 上下文,但是視圖的數(shù)據(jù)模型仍然保留,這也就是首個(gè)視圖“復(fù)活”的原因。
第二次實(shí)驗(yàn)
我們將所有的 Graph3dView 都綁定到一個(gè) window.dataModel 上。具體可以參考下圖:
同實(shí)驗(yàn)一,我們也通過(guò)按鈕創(chuàng)建多個(gè)視圖,在告警后刪除最后一個(gè)視圖??梢园l(fā)現(xiàn),當(dāng)刪除了最后一個(gè) Graph3dView ,第一個(gè)場(chǎng)景也并沒(méi)有恢復(fù)。
我們從 Performance 面板中觀察到整個(gè)過(guò)程:
■當(dāng)首個(gè) WebGL 上下文被銷毀后,JS 堆內(nèi)存出現(xiàn)明顯下降
■刪除最新的視圖后,首個(gè) WebGL 沒(méi)有恢復(fù),且刪除后事件監(jiān)聽器占用的內(nèi)存也沒(méi)有出現(xiàn)下降的情況
照第一組實(shí)驗(yàn)的結(jié)論來(lái)說(shuō),只要數(shù)據(jù)模型還在,視圖應(yīng)當(dāng)“復(fù)活”,但是視圖并沒(méi)有“復(fù)活”。
為什么會(huì)出現(xiàn)上述兩種情況?這是因?yàn)榈诙?,并沒(méi)有正確地將 Graph3dView 清除??梢钥匆幌聝纱螌?shí)驗(yàn)系統(tǒng)的內(nèi)存對(duì)象引用關(guān)系。
第一次
第二次
第一次實(shí)驗(yàn),頁(yè)面上有 19 個(gè) Graph3dView,在內(nèi)存中看到有 19 個(gè) Graph3dView 對(duì)象,而第二次頁(yè)面上僅有 7 個(gè) Graph3dView,但是內(nèi)存中有 17 個(gè) Graph3dView 對(duì)象。這就說(shuō)明了第二次的 Graph3dView 并沒(méi)有被正確垃圾回收,這也就導(dǎo)致了即使移除了一個(gè) Graph3dView,第一個(gè)場(chǎng)景也并不會(huì)恢復(fù)。打開其中一個(gè) Graph3dView 可以看到,Graph3dView 與 window.dataModel 存在引用關(guān)系導(dǎo)致的。
解決方案
從上述的對(duì)照試驗(yàn)中,可以看出使用全局變量存儲(chǔ)視圖實(shí)例是導(dǎo)致內(nèi)存泄漏的主要原因。當(dāng)多個(gè) Graph3dView 共享同一個(gè)全局 dataModel 時(shí),即使刪除視圖,由于全局引用依然存在,這些視圖無(wú)法被垃圾回收。
針對(duì)于內(nèi)存泄漏可以通過(guò)以下幾個(gè)方案解決:
1、避免全局變量引用
該方案從業(yè)務(wù)架構(gòu)層面上解決內(nèi)存泄漏問(wèn)題,可采用以下實(shí)現(xiàn)方式:
■使用模塊化設(shè)計(jì)代替全局變量存儲(chǔ)
■采用弱引用等機(jī)制管理視圖對(duì)象
■建立專門的視圖管理器統(tǒng)一管理實(shí)例
2、視圖復(fù)用機(jī)制
從實(shí)驗(yàn)上可以看出,頻繁創(chuàng)建和銷毀視圖會(huì)帶來(lái)顯著的性能損耗。在實(shí)際的業(yè)務(wù)場(chǎng)景中,可以通過(guò)復(fù)用視圖來(lái)提升性能。在切換視圖時(shí),僅需要通過(guò) dataModel.clear() 清空數(shù)據(jù)模型,重新對(duì)視圖進(jìn)行反序列化即可。
3、資源釋放
對(duì)于必須要頻繁創(chuàng)建/銷毀視圖的特殊場(chǎng)景,可在銷毀前執(zhí)行以下操作:
const dm = new ht.DataModel();
view.setDataModel(dm);
view.dispose();
關(guān)鍵要點(diǎn):
■創(chuàng)建新的 dataModel 實(shí)例替換原有引用
■有效解除視圖與業(yè)務(wù)數(shù)據(jù)的關(guān)聯(lián)關(guān)系
■3D 視圖上存在 dispose 方法,用于主動(dòng)釋放 gl 的資源
需要注意:在具體的項(xiàng)目中,優(yōu)先考慮上兩個(gè)方案,此方案適用于必須銷毀視圖的特殊情況。
4、事件管理優(yōu)化
在處理模塊通信上,可以考慮使用 HT 的事件派發(fā)器進(jìn)行。項(xiàng)目全局上創(chuàng)建一個(gè)事件派發(fā)器,模塊間消息傳遞使用派發(fā)器進(jìn)行:
const notifier = new ht.Notifier();
const func = function(e) {}
notifier.add(func); // 添加監(jiān)聽函數(shù)
notifier.remove(func); // 刪除監(jiān)聽函數(shù)
notifier.fire(func); // 派發(fā)事件
關(guān)鍵要點(diǎn):
■統(tǒng)一使用事件派發(fā)器進(jìn)行跨模塊通信,避免不同模塊間的直接調(diào)用、依賴,減少內(nèi)存泄漏風(fēng)險(xiǎn)
■若模塊需要銷毀,在銷毀前需移除相關(guān)事件監(jiān)聽
■必須使用具名函數(shù)而非匿名函數(shù)作為事件處理器
在前端開發(fā)過(guò)程中,開發(fā)者應(yīng)持續(xù)關(guān)注內(nèi)存變化。內(nèi)存泄漏問(wèn)題并非都是 “爆發(fā)式顯現(xiàn)”,更多是 “漸進(jìn)式累積” —— 初期往往難以察覺(jué),但隨著時(shí)間推移,過(guò)高的內(nèi)存占用會(huì)直接拖慢運(yùn)行性能;對(duì)于基于 WebGL 的應(yīng)用,甚至可能引發(fā)上下文丟失、頁(yè)面白屏等嚴(yán)重問(wèn)題。因此,對(duì)待內(nèi)存泄漏,我們必須保持常態(tài)化關(guān)注的心態(tài)。
審核編輯 黃宇
-
Canvas
+關(guān)注
關(guān)注
0文章
21瀏覽量
11354 -
內(nèi)存泄露
+關(guān)注
關(guān)注
0文章
7瀏覽量
2127
發(fā)布評(píng)論請(qǐng)先 登錄
圖撲 HT 技術(shù)賦能智慧畜牧三維可視化:架構(gòu)設(shè)計(jì)與實(shí)踐應(yīng)用

【HarmonyOS next】ArkUI-X休閑益智消消樂(lè)【進(jìn)階】
基于 HT 2D&3D 渲染引擎的新能源充電樁可視化運(yùn)營(yíng)系統(tǒng)技術(shù)剖析

分享之前使用HarmonyOS NEXT Canvas做的動(dòng)態(tài)GIF視頻的一個(gè)案例,沒(méi)有感情,全是技術(shù)。
Meta因數(shù)據(jù)泄露被愛(ài)爾蘭監(jiān)管機(jī)構(gòu)重罰2.51億歐元
OpenAI正式推出Canvas:寫作編碼新平臺(tái),支持Python
什么是虛擬內(nèi)存分頁(yè) Windows系統(tǒng)虛擬內(nèi)存優(yōu)化方法
虛擬內(nèi)存不足如何解決 虛擬內(nèi)存和物理內(nèi)存的區(qū)別
虛擬內(nèi)存的作用和原理 如何調(diào)整虛擬內(nèi)存設(shè)置
DDR5內(nèi)存與DDR4內(nèi)存性能差異
DDR內(nèi)存的工作原理與結(jié)構(gòu)
如何檢測(cè)DDR內(nèi)存性能
HBM與GDDR內(nèi)存技術(shù)全解析

海量數(shù)據(jù)處理需要多少RAM內(nèi)存
Linux內(nèi)存泄露案例分析和內(nèi)存管理分享

評(píng)論