在 Javascript 中减少垃圾收集器活动的最佳实践

IT技术 javascript garbage-collection
2021-03-10 22:08:34

我有一个相当复杂的 Javascript 应用程序,它有一个每秒调用 60 次的主循环。似乎有很多垃圾收集正在进行(基于 Chrome 开发工具中内存时间线的“锯齿”输出) - 这通常会影响应用程序的性能。

因此,我正在尝试研究减少垃圾收集器必须执行的工作量的最佳实践。(我在网上找到的大部分信息都与避免内存泄漏有关,这是一个略有不同的问题 - 我的内存正在被释放,只是垃圾收集过多。)我假设这主要归结为尽可能多地重用对象,但问题当然在于细节。

该应用程序按照John Resig 的 Simple JavaScript Inheritance以“类”结构化

我认为一个问题是某些函数每秒可以调用数千次(因为它们在主循环的每次迭代中被使用数百次),而且这些函数中的局部工作变量(字符串、数组等)可能会被调用。可能是问题所在。

我知道更大/更重对象的对象池(我们在一定程度上使用它),但我正在寻找可以全面应用的技术,尤其是与在紧密循环中被多次调用的函数相关的技术.

我可以使用哪些技术来减少垃圾收集器必须完成的工作量?

而且,也许还有 - 可以采用哪些技术来识别哪些对象被垃圾回收最多?(这是一个非常大的代码库,所以比较堆的快照并不是很有成效)

4个回答

您需要做的很多事情来最小化 GC 流失,这与大多数其他场景中被认为是惯用的 JS 背道而驰,因此在判断我给出的建议时请记住上下文。

分配发生在现代解释器的几个地方:

  1. 当您通过new或通过文字语法创建对象时[...],或{}.
  2. 当您连接字符串时。
  3. 当您输入包含函数声明的范围时。
  4. 当您执行触发异常的操作时。
  5. 对函数表达式求值时:(function (...) { ... }).
  6. 当您执行强制对象的操作时,例如Object(myNumber)Number.prototype.toString.call(42)
  7. 当你调用一个在引擎盖下执行任何这些的内置函数时,比如Array.prototype.slice.
  8. 当你使用arguments反射过参数列表时。
  9. 当您拆分字符串或与正则表达式匹配时。

避免这样做,并在可能的情况下池化和重用对象。

具体来说,寻找机会:

  1. 将不依赖或几乎不依赖于封闭状态的内部函数拉到更高、寿命更长的范围中。(像Closure 编译器这样的一些代码压缩可以内联内部函数,并可能提高你的 GC 性能。)
  2. 避免使用字符串来表示结构化数据或用于动态寻址。特别避免重复解析 usingsplit或正则表达式匹配,因为每个都需要多个对象分配。这经常发生在查找表和动态 DOM 节点 ID 的键中。例如,lookupTable['foo-' + x]并且document.getElementById('foo-' + x)都因为有一个字符串连接涉及的分配。通常,您可以将键附加到长期存在的对象上,而不是重新连接。根据您需要支持的浏览器,您或许可以Map直接使用对象作为键。
  3. 避免在正常代码路径上捕获异常。而不是try { op(x) } catch (e) { ... },做if (!opCouldFailOn(x)) { op(x); } else { ... }
  4. 当您无法避免创建字符串时,例如将消息传递给服务器,请使用类似JSON.stringify使用内部本机缓冲区来累积内容而不是分配多个对象的内置函数
  5. 避免对高频事件使用回调,并且在可以的情况下,将长期存在的函数(参见 1)作为回调传递,该函数从消息内容重新创建状态。
  6. 避免使用arguments必须在调用时创建类数组对象的函数。

我建议使用JSON.stringify来创建传出网络消息。使用解析输入消息JSON.parse显然涉及分配,其中很多是用于大消息。如果您可以将传入的消息表示为原语数组,那么您可以节省大量分配。您可以围绕它构建不分配的解析器的唯一其他内置函数是String.prototype.charCodeAt. 一个复杂格式的解析器,只使用它,虽然读起来会很糟糕。

精彩的回答,谢谢!为赏金到期深表歉意-当时我正在旅行,由于某种原因,我无法使用手机上的 gmail 帐户登录 SO....:/
2021-04-19 22:08:34
@Bergi,这取决于属性名称是否需要单独分配,但是生成事件而不是解析树的解析器不会产生无关的分配。
2021-04-23 22:08:34
您不认为JSON.parsed 对象分配的空间比消息字符串少(或相等)吗?
2021-05-03 22:08:34
为了弥补赏金的糟糕时机,我添加了一个额外的奖励(200 是我能给的最低限度;) - 出于某种原因,尽管它要求我在奖励它之前等待 24 小时(即使我选择了“奖励现有答案”)。明天就是你的了...
2021-05-09 22:08:34
@UpTheCreek,不用担心。我很高兴你发现它很有用。
2021-05-10 22:08:34

Chrome开发者工具对追踪内存分配一个非常不错的功能。它被称为记忆时间轴。 这篇文章描述了一些细节。我想这就是你所说的“锯齿”?这是大多数 GC 运行时的正常行为。分配继续进行,直到达到触发收集的使用阈值。通常在不同的阈值有不同种类的集合。

Chrome 中的内存时间轴

垃圾收集及其持续时间包含在与跟踪关联的事件列表中。在我相当旧的笔记本上,临时收集发生在大约 4Mb 并且需要 30 毫秒。这是 60Hz 循环迭代中的 2 次。如果这是动画,则 30 毫秒的集合可能会导致卡顿。您应该从这里开始查看您的环境中发生了什么:收集阈值在哪里以及您的收集需要多长时间。这为您提供了评估优化的参考点。但是您可能不会比通过减慢分配速率、延长集合之间的间隔来减少卡顿的频率做得更好。

下一步是使用 Profiles | 记录堆分配功能可按记录类型生成分配目录。这将快速显示跟踪期间哪些对象类型消耗的内存最多,这相当于分配率。按比率降序关注这些。

这些技术不是火箭科学。当您可以使用未装箱的物品时,请避免装箱的物品。使用全局变量来保存和重用单个装箱对象,而不是在每次迭代中分配新的对象。在空闲列表中汇集公共对象类型而不是放弃它们。缓存字符串连接结果,可能会在未来的迭代中重用。通过在封闭范围内设置变量来避免分配只是为了返回函数结果。您必须在其自己的上下文中考虑每种对象类型才能找到最佳策略。如果您需要有关具体问题的帮助,请发布描述您正在查看的挑战的详细信息的编辑。

我建议不要在整个应用程序中歪曲您的正常编码风格,以试图产生更少的垃圾。这与您不应过早优化速度的原因相同。您的大部分努力加上代码的大部分增加的复杂性和晦涩性都将毫无意义。

关于“过早优化是万恶之源”:了解。不要盲目跟风。在某些情况下,例如游戏和多媒体编程,性能是最重要的,您将拥有大量“热门”代码。所以是的,你将不得不调整你的编程风格。
2021-04-25 22:08:34
是的,这就是我所说的锯齿。我知道总会有某种锯齿图案,但我担心的是,我的应用程序的锯齿频率和“悬崖”非常高。有趣的是,GC事件没有在我的时间表显示-显示在“记录”窗格(中间的一个)的唯一事件是:request animation frameanimation frame fired,和composite layers我不知道为什么我看的GC Event不像你(这是在最新版本的 chrome 上,还有金丝雀)。
2021-05-05 22:08:34
我已经尝试使用带有“记录堆分配”的分析器,但到目前为止还没有发现它非常有用。也许这是因为我不知道如何正确使用它。它似乎充满了对我来说毫无意义的参考资料,例如@342342code relocation info
2021-05-09 22:08:34

作为一般原则,您希望尽可能多地缓存,并尽可能少地为每次循环运行创建和销毁。

我脑海中浮现的第一件事是减少在主循环中使用匿名函数(如果有的话)。此外,很容易陷入创建和销毁传递给其他函数的对象的陷阱。我绝不是 javascript 专家,但我可以想象:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

运行速度会比这快得多:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

你的程序有停机时间吗?也许您需要它平稳运行一两秒钟(例如动画),然后它有更多的时间来处理?如果是这种情况,我可以看到在整个动画中通常会被垃圾收集的对象,并在某个全局对象中保留对它们的引用。然后当动画结束时,您可以清除所有引用并让垃圾收集器完成它的工作。

很抱歉,与您已经尝试和想到的相比,这有点微不足道。

这。此外,其他函数(不是 IIFE)中提到的函数也是常见的滥用,会消耗大量内存并且很容易遗漏。
2021-04-20 22:08:34
谢谢克里斯!不幸的是,我没有任何停机时间:/
2021-05-02 22:08:34

我会在其中创建一个或几个对象global scope(我确定不允许垃圾收集器接触它们),然后我会尝试重构我的解决方案以使用这些对象来完成工作,而不是使用局部变量.

当然,它不能在代码的任何地方都完成,但通常这是我避免垃圾收集器的方法。

PS 这可能会使代码的特定部分不太容易维护。

GC 一致地取出我的全局范围变量。
2021-05-05 22:08:34