带有对象池的静态内存 JavaScript

发布于 2023-01-08 19:19:35 字数 6067 浏览 122 评论 0

所以你收到一封电子邮件,说你的网络游戏/网络应用程序在一段时间后表现如何糟糕,你挖掘你的代码,没有看到任何突出的东西,直到你打开 Chrome 的内存性能工具,并且看到这个:

我想知道那些锯齿是什么?

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

在内存图视图中,这种锯齿状模式非常能说明潜在的关键性能问题。 随着内存使用量的增加,您会看到时间线捕获中的图表区域也在增加。 当图表突然下降时,这是垃圾收集器运行并清理了引用的内存对象的实例。

  看看所有这些 GC 事件!

在这样的图表中,您可以看到发生了很多垃圾收集事件,这可能会损害您的网络应用程序的性能。 本文将讨论如何控制内存使用,减少对性能的影响。

垃圾收集和性能成本

JavaScript 的 内存模型 建立在称为 垃圾收集器 的技术之上。 在许多语言中,程序员直接负责从系统的 内存堆中 分配和释放内存。 然而,垃圾收集器系统代表程序员管理这个任务,这意味着当程序员取消引用对象时,对象不会直接从内存中释放,而是在稍后 GC 的启发式决定这样做是有益的时候所以。 这个决策过程需要 GC 对活动和非活动对象执行一些统计分析,这需要一段时间才能执行。

在计算机科学中, 垃圾收集器 (GC) 是一种自动内存管理形式。 垃圾收集器尝试回收垃圾,或回收程序不再使用的对象占用的内存。

垃圾收集通常被描绘成手动内存管理的对立面,它需要程序员指定要释放哪些对象并将其返回到内存系统

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

高性能 应用程序依赖于一致的性能边界来确保用户获得流畅的体验。 垃圾收集器系统可能会绕过这个目标,因为它们可以在随机时间随机运行,占用应用程序满足其性能目标所需的可用时间。

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

如前所述,一旦一组试探法确定有足够多的非活动对象,一个脉冲将是有益的,GC 脉冲就会发生。 因此,减少垃圾收集器从您的应用程序中占用的时间的关键在于尽可能多地消除过度创建和释放对象的情况。 这种频繁创建/释放对象的过程称为“内存流失”。 如果您可以在应用程序的生命周期内减少内存流失,那么您也可以减少 GC 从执行中花费的时间。 这意味着您需要删除/减少创建和销毁对象的数量,实际上,您必须停止分配内存。 此过程将从以下位置移动您的内存图:

我想知道那些锯齿是什么?

对此:

  啊哈,这样更好。

在这个模型中,你可以看到图形不再是 锯齿状 的模式,而是在开始时增长很大,然后随着时间的推移慢慢增长。 如果您因内存流失而遇到性能问题,这就是您要创建的图表类型。

转向静态内存 JavaScript

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

  1. 检测您的应用程序以确定各种使用场景所需的最大活动内存对象数(每种类型)
  2. 重新实现您的代码以预分配最大数量,然后手动获取/释放它们而不是转到主内存。

实际上,完成#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 DevRel Renato Mangini 同事指出的那样,它有一些缺点。

池并不适合所有人,即使是在高性能应用程序上也是如此。 在采用对象池和静态内存方法之前考虑以下权衡: 启动时间会更长,因为您在初始化时花费了分配内存的周期。 内存在低使用场景下不缩水; 你的应用程序会贪婪地消耗内存。 有时您需要在将对象返回到池中时对其进行清理,这在内存流失率高的区域可能会带来不小的开销。

结论

JavaScript 非常适合 Web 的原因之一在于它是一种快速、有趣且易于上手的语言。 这主要是由于它对语法限制的低门槛以及它代表您处理内存问题。 您可以编写代码并让它处理脏活。 然而,对于高性能 Web 应用程序(如 HTML5 游戏 ),GC 经常会以非常需要 的帧率 消耗掉,从而降低最终用户的体验。 通过一些仔细的检测和对象池的采用,您可以减少帧速率的负担,并回收时间来做更棒的事情。

源代码

Web 上有很多对象池的实现,所以我不会再用另一个让你厌烦了。 相反,我将指导您了解这些,每个都有具体的实施细微差别; 考虑到每个应用程序的使用可能有特定的实现需求,这一点很重要。

参考

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

飘过的浮云

暂无简介

0 文章
0 评论
537 人气
更多

推荐作者

胡图图

文章 0 评论 0

zt006

文章 0 评论 0

z祗昰~

文章 0 评论 0

冰葑

文章 0 评论 0

野の

文章 0 评论 0

天空

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文