簡介
因此,當您收到電子郵件,指出您的網路遊戲 / 網路應用程式在一段時間後效能不佳時,您會仔細檢查程式碼,但不會發現任何異常,直到您開啟 Chrome 的記憶體效能工具,才會看到以下畫面:

同事笑了,因為他們發現您有記憶體相關的效能問題。
在記憶體圖表檢視畫面中,這種鋸齒狀模式很能說明可能發生的嚴重效能問題。隨著記憶體用量增加,您會發現時間軸擷取畫面中的圖表區域也會隨之增加。當圖表突然下降時,表示垃圾收集器已執行,並清理參照的記憶體物件。

在類似圖表中,您可以看到發生許多垃圾收集事件,這可能會對網頁應用程式效能造成不利影響。本文將說明如何控管記憶體用量,降低對效能造成的影響。
垃圾收集和效能成本
JavaScript 的記憶體模型是建立在「垃圾收集器」技術上。在許多語言中,程式設計師直接負責從系統的記憶體堆積中分配及釋出記憶體。不過,垃圾收集器系統會代表程式設計師管理這項工作,也就是說,物件不會在程式設計師解析參照時直接從記憶體中釋放,而是在 GC 的啟發式演算法判定這麼做有益時,才會在稍後釋放。這個決策程序要求 GC 對活動和非活動物件執行一些統計分析,這需要一段時間才能完成。
垃圾收集通常被視為手動記憶體管理的反義,後者要求程式設計師指定要釋放哪些物件,並將其傳回記憶體系統
GC 回收記憶體的程序並非免費,通常會花費一段時間執行作業,進而影響可用的效能;此外,系統本身會決定何時執行。您無法控制這項動作,垃圾收集脈衝可能在程式碼執行期間隨時發生,並會阻斷程式碼執行,直到完成為止。您通常無法得知這項脈衝的時間長度,因為這會根據程式在任何特定時間點的記憶體使用方式,需要一段時間才能執行。
高效能應用程式需要維持一致的效能界限,才能確保使用者享有流暢的體驗。垃圾收集器系統可能會短路這個目標,因為它們可能會在隨機時間執行隨機時間長度,進而影響應用程式需要符合效能目標的可用時間。
減少記憶體流失,降低垃圾收集稅
如前所述,一旦一組啟發法的判斷結果顯示有足夠的非活動物件,就會觸發 GC 脈衝。因此,要縮短垃圾收集器在應用程式中所需的時間,關鍵在於盡可能減少不必要的物件建立和釋放作業。這個經常建立/釋放物件的程序稱為「記憶體抖動」。如果您能在應用程式生命週期內減少記憶體抖動,GC 執行所需的時間也會隨之減少。這表示您需要移除 / 減少建立和銷毀的物件數量,也就是說,您必須停止分配記憶體。
這項程序會將記憶體圖表從以下位置移出:

改為:

在這個模型中,您可以看到圖表不再呈現鋸齒狀的模式,而是一開始大幅成長,然後隨著時間推移緩慢增加。如果記憶體流失導致效能問題,您可以建立這類圖表。
朝著靜態記憶體 JavaScript 邁進
Static Memory JavaScript 是一種技術,可在應用程式啟動時預先配置應用程式在其生命週期中所需的所有記憶體,並在執行期間管理該記憶體,以便在不再需要物件時釋放記憶體。我們可以透過幾個簡單步驟達成這個目標:
- 檢測應用程式,以便在各種用途情境下,判斷所需的記憶體物件 (每個類型) 數量上限
- 重新實作程式碼,預先配置該最大值,然後手動擷取/釋放,而非進入主記憶體。
實際上,要完成 #1 就必須進行一些 #2 的作業,因此我們就從這裡開始。
物件集區
簡單來說,物件資源池是指保留一組共用類型的未使用物件。當您需要為程式碼提供新物件時,請改為從集區中回收未使用的物件,而非從系統的記憶體堆積中分配新物件。外部程式碼使用完物件後,會將物件傳回集區,而不是釋出至主記憶體。由於物件從未從程式碼中解析 (也就是刪除),因此不會進行垃圾收集。使用物件集區可讓程式設計師控制記憶體,減少垃圾收集器對效能造成的影響。
由於應用程式會維護一組異質的物件類型,因此您必須為每個類型建立一個物件集區,才能妥善使用物件集區,並在應用程式執行期間經歷高流失率。
var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};
//..... do some stuff with the object that we need to do
gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference
對於大多數應用程式而言,您最終都會需要分配新物件。在多次執行應用程式後,您應該可以掌握這個上限,並在應用程式啟動時預先配置該數量的物件。
預先配置物件
在專案中實作物件資源池,可讓您在應用程式執行階段中,取得所需物件數量的理論上限。在網站執行各種測試情境後,您就能充分掌握所需記憶體需求的類型,並在某處編目這些資料,然後分析這些資料,瞭解應用程式的記憶體需求上限。
接著,您可以在應用程式的發布版本中,將初始化階段設為預先填入所有物件集合的指定數量。這項操作會將所有物件初始化推送至應用程式前端,並減少執行期間動態發生的配置數量。
function init() {
//preallocate all our pools.
//Note that we keep each pool homogeneous wrt object types
gEntityObjectPool.preAllocate(256);
gDomObjectPool.preAllocate(888);
}
您選擇的金額會大大影響應用程式的行為;有時理論上的最高值並非最佳選項。舉例來說,選擇平均最大值可能會為非重度使用者提供較小的記憶體占用空間。
並非萬靈丹
在整個應用程式分類中,靜態記憶體成長模式可帶來優勢。不過,Chrome 開發人員關係經理 Renato Mangini 指出,這項做法有一些缺點。
結論
JavaScript 是理想的網頁語言之一,原因在於它快速、有趣且易於上手。這主要是因為語法限制門檻低,且會代為處理記憶體問題。您可以編寫程式碼,讓它處理繁重的工作。不過,對於高效能網路應用程式 (例如 HTML5 遊戲),GC 通常會耗用至關重要的影格速率,進而降低使用者體驗。透過謹慎的檢測和採用物件集區,您可以減少這項工作對影格速率的負擔,並將時間用於更棒的功能。
原始碼
網路上有許多物件集區的實作方式,因此我不會再提及另一種實作方式來打擾您。相反地,我會引導您前往這些頁面,每個頁面都提供特定的實作細節,這點非常重要,因為每種應用程式用途可能都有特定的實作需求。
- Gamecore.js 的物件集區
- Beej 的物件集區
- Emehrkay 的超簡單物件集區
- Steven Lambert 的遊戲專用物件集區
- RenderEngine 的 objectPool 設定