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


94

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

因此,我正在尝试研究最佳实践,以减少垃圾收集器要做的工作量。(我在网络上能够找到的大多数信息都考虑到避免内存泄漏,这是一个稍微不同的问题-我的内存正在释放,只是正在进行太多的垃圾收集。)这主要归结为尽可能多地重用对象,但是魔鬼在细节上。

该应用程序按照John Resig的Simple JavaScript Inheritance的 “类”结构进行构造。

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

我知道较大/较重对象的对象池化(并且在一定程度上使用了这种方法),但是我正在寻找可以广泛应用的技术,特别是在紧密循环中多次调用的函数。

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

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


2
您是否有示例代码可以向我们展示?然后,这个问题将更容易回答(但也可能不太普遍,因此我不确定在这里)
John Dvorak

2
如何每秒停止运行数千次功能?那真的是解决这个问题的唯一方法吗?这个问题似乎是一个XY问题。你所描述的X,但你真正需要的是Y的解决方案
特拉维斯Ĵ

2
@TravisJ:他每秒只运行60次,这是相当普遍的动画速率。他不是要减少工作量,而是要提高垃圾收集效率。
Bergi

1
@Bergi-“某些功能每秒可调用数千次”。那是每毫秒一次(可能更糟!)。这根本不常见。每秒60次应该不是问题。这个问题过于笼统,只会引起意见或猜测。
特拉维斯J

4
@TravisJ-在游戏框架中并不少见。
UpTheCreek

Answers:


127

在大多数其他情况下,您需要做的许多事情来最大程度地减少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. 避免使用字符串表示结构化数据或用于动态寻址。尤其要避免重复使用split或正则表达式匹配进行解析,因为每个匹配都需要多个对象分配。查找表的键和动态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。但是,仅使用一种复杂格式的解析器将很难理解。


您不认为JSON.parsed对象分配的空间少于(或等于)消息字符串吗?
Bergi

@Bergi,这取决于属性名称是否需要单独的分配,但是生成事件而不是解析树的解析器不会产生多余的分配。
Mike Samuel

很棒的答案,谢谢!悬赏到期的许多道歉-当时我正在旅行,由于某种原因我无法使用我的gmail帐户登录手机.... ::
UpTheCreek 2013年

为了弥补赏金计划的糟糕时机,我添加了一个额外的赏金计划(我最多只能给200欧元;)-出于某种原因,尽管它要求我等24小时才能获得赏金(即使我选择了“奖励现有答案”)。明天会属于你的...
UpTheCreek 2013年

@UpTheCreek,不用担心。很高兴您发现它有用。
Mike Samuel

13

Chrome开发者工具对追踪内存分配一个非常不错的功能。它称为“内存时间轴”。 本文介绍了一些详细信息。我想这就是您所说的“锯齿”?这是大多数GC运行时的正常行为。分配继续进行,直到达到使用量阈值触发收集。通常,在不同的阈值下会有不同种类的集合。

Chrome中的内存时间轴

垃圾收集与跟踪及其持续时间一起包含在与跟踪关联的事件列表中。在我比较旧的笔记本上,临时收集的速度大约为4Mb,耗时30ms。这是您的60Hz循环迭代中的2次。如果这是动画,则30ms的集合可能会造成结结。您应该从这里开始,看看您的环境正在发生什么:收集阈值在哪里以及收集需要多长时间。这为您提供评估优化的参考点。但是您可能不会做得比通过降低分配速率,延长两次收集之间的间隔来减少口吃的频率更好。

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

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

我建议不要在a弹枪中减少整个应用程序的正常编码样式,以减少垃圾的产生。出于同样的原因,您不应该过早地优化速度。您的大部分工作以及所增加的代码复杂性和晦涩性将变得毫无意义。


是的,这就是我所说的锯齿。我知道总是会有某种锯齿模式,但是我担心的是,使用我的应用程序,锯齿频率和“悬崖”都很高。有趣的是,GC事件没有在我的时间表显示-显示在“记录”窗格(中间的一个)的唯一事件是:request animation frameanimation frame fired,和composite layers。我不知道为什么我看不到GC Event你(这是最新版的chrome,也有金丝雀)。
UpTheCreek 2013年

4
我已经尝试将探查器与“记录堆分配”一起使用,但到目前为止尚未发现它非常有用。也许是因为我不知道如何正确使用它。它似乎充满了对我来说毫无意义的参考资料,例如@342342code relocation info
UpTheCreek 2013年

9

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

我想到的第一件事是减少主循环内匿名函数(如果有)的使用。同样很容易陷入创建和销毁传递给其他函数的对象的陷阱。我绝不是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)中提到的功能,也是常见的滥用行为,会消耗大量内存,并且容易丢失。
Esailija

谢谢克里斯!不幸的是,我没有任何停机时间:/
UpTheCreek 2013年

4

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

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

PS这可能会使代码的特定部分难于维护。


GC会一致地取出我的全局范围变量。
VectorVortec
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.