简介
假设您收到一封电子邮件,其中指出您的网页游戏/Web 应用在运行一段时间后性能不佳。您仔细检查代码,没有发现任何异常,直到您打开 Chrome 的内存性能工具,看到以下内容:
您的一位同事笑了,因为他们意识到您遇到了与内存相关的性能问题。
在内存图表视图中,这种锯齿状模式非常明显地表明存在潜在的严重性能问题。随着内存用量增加,您会看到时间轴捕获中的图表区域也会增大。当图表突然下降时,表示垃圾回收器已运行并清理了引用的内存对象。
在这样的图表中,您可以看到发生了许多垃圾回收事件,这可能会对 Web 应用的性能造成不利影响。本文将介绍如何控制内存用量,从而减少对性能的影响。
垃圾回收和性能开销
JavaScript 的内存模型基于一种称为垃圾回收器的技术。在许多语言中,程序员直接负责从系统的内存堆中分配和释放内存。不过,垃圾回收器系统会代表程序员管理此任务,这意味着当程序员取消引用对象时,系统不会直接从内存中释放对象,而是在稍后 GC 的启发式算法确定这样做有益时释放。此决策过程要求 GC 对活跃和非活跃对象执行一些统计分析,这需要花费一些时间来执行。
垃圾回收通常被描述为手动内存管理的对立面,后者需要程序员指定要取消分配并返回到内存系统的对象
GC 回收内存的过程并非免费,它通常会占用一段时间来执行其工作,从而降低可用性能;除此之外,系统本身会决定何时运行。您无法控制此操作,GC 脉冲可能在代码执行期间随时发生,这会阻塞代码执行,直到垃圾回收完成。您通常不知道此脉冲的持续时间;它需要一些时间来运行,具体取决于您的程序在任何给定时间点如何利用内存。
高性能应用依赖于一致的性能边界,以确保为用户提供流畅的体验。垃圾回收器系统可能会阻碍实现此目标,因为它们可能会在随机时间运行随机时长,从而占用应用实现其效果目标所需的可用时间。
减少内存抖动,降低垃圾回收开销
如前所述,当一组启发法确定有足够多的非活跃对象,且执行 GC 脉冲会有益时,就会发生 GC 脉冲。因此,要减少垃圾回收器从应用中占用的时间,关键在于尽可能消除过度创建和释放对象的情况。这种频繁创建/释放对象的过程称为“内存抖动”。如果您可以在应用的生命周期内减少内存抖动,那么您还可以减少 GC 在执行过程中所花费的时间。这意味着您需要移除/减少创建和销毁的对象数量,实际上,您必须停止分配内存。
此过程会将您的记忆图谱从以下位置:
更改为:
在此模型中,您可以看到图表不再具有锯齿状模式,而是在开始时大幅增长,然后随着时间的推移缓慢增加。如果您因内存抖动而遇到性能问题,则需要创建此类图表。
向静态内存 JavaScript 迈进
静态内存 JavaScript 是一种技术,涉及在应用启动时预分配其生命周期内所需的所有内存,并在执行期间管理该内存,因为不再需要对象。我们可以通过几个简单的步骤来实现这一目标:
- 对应用进行插桩,以确定一系列使用场景下所需的最大活跃内存对象数量(按类型)
- 重新实现代码,以预分配该最大量,然后手动提取/释放它们,而不是转到主内存。
实际上,要实现第 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 开发,原因之一在于它是一种快速、有趣且易于上手的语言。这主要是因为 Python 的语法限制较少,并且可以代您处理内存问题。您可以专注于编写代码,让它来处理繁琐的工作。 不过,对于高性能 Web 应用(例如 HTML5 游戏),GC 通常会占用急需的帧速率,从而降低最终用户的体验。通过一些细致的插桩和对象池的使用,您可以减轻帧速率的负担,并将这些时间用于更棒的事情。
源代码
网上有很多对象池的实现,因此我不会再向您介绍其他实现。我会直接向您介绍这些方法,每种方法都有特定的实现细节;考虑到每种应用场景可能有特定的实现需求,这一点非常重要。