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

UpT*_*eek 90 javascript garbage-collection

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

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

该应用程序按照John Resig的简单JavaScript继承的方式构建在"类"中.

我认为一个问题是一些函数可以每秒调用数千次(因为它们在主循环的每次迭代中使用了数百次),也许这些函数中的本地工作变量(字符串,数组等)可能是问题.

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

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

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

Mik*_*uel 124

在大多数其他场景中,为了最大限度地减少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.一个只使用它的复杂格式的解析器,虽然看起来很难听.


Gen*_*ene 12

Chrome开发人员工具具有非常好的功能,可用于跟踪内存分配.它被称为记忆时间线. 本文介绍了一些细节.我想这就是你所说的"锯齿"?这是大多数GC运行时的正常行为.分配继续进行,直到达到触发收集的使用阈值.通常,在不同的阈值处存在不同种类的集合.

Chrome中的内存时间轴

垃圾收集包含在与跟踪关联的事件列表及其持续时间中.在我相当古老的笔记本上,短暂的收藏发生在大约4Mb并且需要30ms.这是你的60Hz循环迭代中的2个.如果这是一个动画,30ms的收藏可能会造成口吃.您应该从这里开始查看您的环境中发生了什么:收集阈值在哪里以及收集的时间.这为您提供了评估优化的参考点.但是你可能不会通过减慢分配率来减少口吃的频率,延长收集间隔.

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

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

我建议不要在霰弹枪试图产生更少垃圾的整个应用程序中歪曲你的正常编码风格.出于同样的原因,您不应该过早地优化速度.您的大部分努力加上大部分代码的复杂性和模糊性都将毫无意义.

  • 我已经尝试将探查器与'记录堆分配'一起使用,但到目前为止还没有发现它非常有用.也许这是因为我不知道如何正确使用它.它似乎充满了对我来说毫无意义的引用,例如`@ 342342`和`代码重定位信息`. (4认同)
  • 关于“过早优化是万恶之源”:理解。不要只是盲目跟风。在某些场景中,例如游戏和多媒体编程,性能至关重要,并且您将拥有大量“热门”代码。所以,是的,你必须调整你的编程风格。 (2认同)

Chr*_*s B 9

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

我头脑中的第一件事就是减少主循环中匿名函数(如果有的话)的使用.此外,很容易陷入创建和销毁传递到其他函数的对象的陷阱.我绝不是一个javascript专家,但我想这个:

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

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

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}
Run Code Online (Sandbox Code Playgroud)

比这运行得快得多:

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

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}
Run Code Online (Sandbox Code Playgroud)

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

对不起,如果这与你已经尝试过的想法相比有点微不足道了.