使用对象池的静态内存 JavaScript

Colt McAnlis
Colt McAnlis

简介

假设您收到一封电子邮件,其中指出您的 Web 游戏/Web 应用在运行一段时间后性能不佳。您仔细检查了代码,但没有发现任何明显的问题,直到您打开 Chrome 的内存性能工具,看到以下内容:

内存时间轴的快照

您的一位同事笑了,因为他们意识到您遇到了与内存相关的性能问题。

在内存图表视图中,这种锯齿状模式非常能说明可能存在的严重性能问题。随着内存用量的增加,您会在时间轴截图中看到图表区域也会随之增大。如果图表突然下降,则表示垃圾回收器已运行并清理了您引用的内存对象。

锯齿形图标的含义

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

垃圾回收和性能成本

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

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

GC 回收内存的过程并非免费的,它通常会占用一段时间来执行其工作,从而降低可用性能;此外,系统本身会决定何时运行。您无法控制此操作,GC 脉冲可能会在代码执行期间随时发生,并会在完成之前阻塞代码执行。您通常不知道此脉冲的持续时间;它需要运行一段时间,具体取决于您的程序在任何给定时间点内存的使用方式。

高性能应用依靠一致的性能边界确保用户获得流畅的体验。垃圾回收器系统可以缩短实现其性能目标的时间,因为它们可以在随机时间运行随机时长,占用应用程序实现其性能目标所需的可用时间。

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

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

此过程会将内存图表从以下位置移至:

内存时间轴中的快照

更改为:

静态内存 JavaScript

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

朝着静态内存 JavaScript 迈进

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

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

实际上,要实现第 1 点,我们需要完成一些第 2 点的工作,因此我们先从第 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 DevRel 同事 Renato Mangini 指出的那样,这种方法也存在一些缺点。

总结

JavaScript 非常适合 Web 开发,其中一个原因在于它是一门快速、有趣且易于上手的语言。这主要是由于它对语法限制的门槛很低,并且它代表您处理内存问题。您只需编写代码,让它处理繁琐的工作。不过,对于高性能 Web 应用(例如 HTML5 游戏),GC 通常会占用极其重要的帧速率,从而降低最终用户的体验。通过一些仔细的检测和采用对象池,您可以减轻帧速率负担,并腾出时间来处理更重要的工作。

源代码

网络上充斥着大量对象池的实现,我不多说了。不过,我会为您介绍以下几种方法,每种方法都有特定的实现细微之处;鉴于每种应用用例可能都有特定的实现需求,这一点非常重要。

参考