具有物件集區的靜態記憶體 JavaScript

Colt McAnlis
Colt McAnlis

簡介

因此,您會收到一封電子郵件,說明您的網頁遊戲 / 網頁應用程式在特定時間過後成效不佳的情況。在您開啟 Chrome 的記憶體效能工具之前,先查看自己的程式碼,完全沒發現任何值得注意的項目。如下所示:

記憶體時間軸快照

同事笑了,因為他們發現您有記憶體相關的效能問題。

在記憶體圖表檢視畫面中,這種鋸齒狀圖案顯示可能存在嚴重效能問題。隨著記憶體用量增加,時間軸擷取中的圖表區域也會有所成長。如果圖表突然下降,則屬於垃圾收集器執行,並清理參照的記憶體物件。

鋸齒狀圖形代表的意義

在類似圖表中,您可以看到發生許多垃圾收集事件,這可能會對網頁應用程式效能造成不利影響。本文將說明如何控管記憶體用量,降低對效能造成的影響。

垃圾收集和效能成本

JavaScript 的記憶體模型是建立在垃圾收集器技術上。在許多語言中,程式設計師直接負責從系統的記憶體堆積中分配及釋出記憶體。不過,垃圾收集器系統會代表程式設計師管理這項工作,也就是說,當程式設計師解參照物件時,系統不會直接從記憶體釋放物件,而是在 GC 的啟發式決策機制判定這麼做有益時,才會釋放物件。這個決策程序要求 GC 對活動和非活動物件執行一些統計分析,這需要一段時間才能完成。

垃圾收集通常被視為手動記憶體管理的反義,後者要求程式設計師指定要釋放哪些物件,並將其傳回記憶體系統

在回收記憶體的過程中,GC 收回記憶體的程序並不足夠,一般會花一些時間處理工作,同時影響可用的效能;同時,系統本身也會決定何時執行。您無法控制這項動作,垃圾收集脈衝可在程式碼執行期間隨時發生,並會在完成前阻斷程式碼執行。您通常無法得知這項脈衝的時間長度,因為這會根據程式在任何特定時間點的記憶體使用方式,需要一段時間才能執行。

高效能應用程式仰賴一致的效能界線,以確保使用者享有流暢的體驗。垃圾收集器系統可能會短路這個目標,因為它們可能會在隨機時間執行隨機時間長度,進而影響應用程式需要符合效能目標的可用時間。

減少記憶體流失,減少垃圾收集稅

如前所述,一旦一組啟發法的判斷結果顯示有足夠的非活動物件,就會觸發 GC 脈衝。因此,要縮短垃圾收集器在應用程式中所需的時間,關鍵在於盡可能減少不必要的物件建立和釋放作業。這個經常建立/釋放物件的程序稱為「記憶體抖動」。如果您能在應用程式生命週期內減少記憶體抖動,GC 執行所需的時間也會隨之減少。這表示您需要移除 / 減少已建立和刪除的物件數量,實際上必須停止分配記憶體。

這項程序會將記憶體圖表從以下位置移至:

回憶時間軸的快照

改為:

靜態記憶體 JavaScript

在這個模型中,您可以看到圖表不再呈現鋸齒狀的模式,而是一開始大幅成長,然後隨著時間推移緩慢增加。如果記憶體流失導致效能問題,您可以建立這類圖表。

改用靜態記憶體 JavaScript

靜態記憶體 JavaScript 是一種技術,包括在應用程式啟動時預先分配、生命週期所需的所有記憶體,以及在執行期間管理這些記憶體 (因為不再需要物件時)。我們可以透過幾個簡單步驟達成這個目標:

  1. 檢測應用程式,以判斷出於某種使用情境中需要多少個使用中記憶體物件 (每種類型) 的數量上限
  2. 重新實作程式碼,預先配置該最大值,然後手動擷取/釋放,而非進入主記憶體。

實際上,要完成 #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 通常會耗用至關重要的影格速率,進而降低使用者體驗。只要仔細地進行檢測並採用物件集區,就能減輕這項作業對影格速率的負擔,並將時間運用在更重要的工作上。

原始碼

網路上的物件集區有很多實作,所以我不會佔用您太多時間。相反地,我會引導您前往這些頁面,每個頁面都提供具體的實作細節,這點非常重要,因為每種應用程式用途可能都有特定的實作需求。

參考資料