使用对象池的静态内存 JavaScript

Colt McAnlis
Colt McAnlis

简介

因此,您收到一封说明您的网络游戏 / 网络应用在一段时间后表现不佳的电子邮件。您翻阅了代码,没有看到任何突出之处,直到打开 Chrome 的内存性能工具,可以看到以下内容:

内存时间轴的快照

您的一位同事发现您存在与内存相关的性能问题,因此笑了起来。

在内存图视图中,这种锯齿状模式说明了潜在的严重性能问题。随着内存使用量的增加,您会发现时间轴捕获中的图表区域也越来越大。当图表突然下降时,即表示垃圾回收器已运行并清理了引用的内存对象。

锯齿的意义

在这样的图表中,您可以看到发生了大量的垃圾回收事件,这可能对您的 Web 应用的性能造成负面影响。本文介绍了如何控制内存用量,以降低对性能的影响。

垃圾回收和性能成本

JavaScript 的内存模型基于一项称为垃圾回收器的技术。在许多语言中,程序员直接负责从系统的内存堆中分配内存。然而,垃圾回收器系统是代表程序员管理此任务,这意味着程序员解引用对象时,对象不是直接从内存中释放的,而是稍后在 GC 的启发法确定这样做有用时。此决策过程需要 GC 对活跃和不活跃对象执行一些统计分析,这需要一些时间才能执行。

垃圾回收通常被描述为与手动内存管理相反,后者要求程序员指定要取消分配并返回内存系统的对象

GC 回收内存的过程并不是免费的,它通常会花费一些时间来执行工作,从而影响可用的性能;除此之外,由系统本身来决定何时运行。由于您无法控制此操作,代码执行期间可能随时发生 GC 脉冲,这会阻止代码执行,直到完成为止。通常,您不知道此脉冲的持续时间;运行需要一些时间,具体取决于您的程序在任何给定点使用内存的方式。

高性能应用依赖于一致的性能边界,以确保为用户提供流畅的体验。垃圾回收系统可能会使此目标出现短路,因为它们可能会以随机时长的方式随机运行,占用应用实现其性能目标所需的可用时间。

减少内存流失,降低垃圾回收税

如上所述,当一组启发法确定有足够的非活跃对象对脉冲有益时,就会发生 GC 脉冲。因此,减少垃圾回收器从您的应用中花费的时间的关键在于尽可能避免过度创建和释放对象。这种频繁创建/释放对象的过程称为“内存抖动”。如果您可以在应用的生命周期内减少内存抖动,也将减少 GC 的执行时间。这意味着您需要移除 / 减少创建和销毁对象的数量,实际上,您必须停止分配内存。

此过程会将您的内存图表从:

内存时间轴的快照

更改为:

静态内存 JavaScript

在此模型中,您可以看到图表不再具有类似锯齿状的模式,而是在开始时大幅增长,然后随时间缓慢增加。如果您因内存抖动而遇到性能问题,则需要创建的图表类型。

转向静态内存 JavaScript

静态内存 JavaScript 是一种技术,需要在应用开始时预先分配应用生命周期所需的所有内存,并在执行期间管理这些内存(因为不再需要对象)。我们可以通过几个简单的步骤来实现这一目标:

  1. 对您的应用进行插桩处理,以确定各种使用场景所需的活动内存对象(每种类型)的最大数量
  2. 重新实现您的代码以预分配该最大内存量,然后手动提取/释放它们,而不是进入主内存。

实际上,要完成第 1 项操作,我们需要完成第二项操作,那么我们开始吧。

对象池

简单来说,对象池化就是保留一组共用同一类型的未使用的对象的过程。当您的代码需要新对象时,您可以从系统内存堆中回收一个未使用的对象,而不是从池中回收一个未使用的对象。处理完对象后,外部代码会返回到对象池,而不是将其释放到主内存。由于对象绝不会从代码中解引用(即删除),因此不会被垃圾回收。利用对象池,程序员可以重新掌控内存,从而降低垃圾回收器对性能的影响。

由于应用维护的对象类型多种多样,因此正确使用对象池要求您为每种类型使用一个对象池,以便在应用运行时遇到高流失率。

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 DevRel Renato Mangini 所指出的那样,它也存在一些缺点。

总结

JavaScript 之所以适合网络,之所以成为理想的原因之一,是因为 JavaScript 是一种快速、有趣且易于上手的语言。这主要是因为它对语法限制的门槛较低,并且能够代表您处理内存问题。你可以编写代码,让它处理枯燥的工作。然而,对于 HTML5 游戏等高性能 Web 应用,GC 往往会占用至关重要的帧速率,从而降低最终用户的体验。通过一些谨慎的插桩和对象池的采用,您可以减轻帧速率的负担,并利用这些时间来打造更棒的功能。

源代码

网络上随处可见大量对象池的实现,所以我就不多演示了。相反,由于每种应用使用方式可能都有特定的实现需求,因此我会引导您了解每种应用的具体实现方式,这一点非常重要。

参考编号