我有一个相当复杂的 Javascript 应用程序,它有一个每秒调用 60 次的主循环。似乎有很多垃圾收集正在进行(基于 Chrome 开发工具中内存时间线的“锯齿”输出)——这通常会影响应用程序的性能。
因此,我正在尝试研究减少垃圾收集器必须做的工作量的最佳实践。 (我在网上找到的大部分信息都是关于避免内存泄漏,这是一个稍微不同的问题——我的内存正在被释放,只是垃圾收集太多了。)我假设这主要归结为尽可能多地重用对象,但细节当然是魔鬼。
该应用程序按照 John Resig's Simple JavaScript Inheritance 的行在“类”中构建。
我认为一个问题是某些函数每秒可以调用数千次(因为它们在主循环的每次迭代中使用了数百次),并且这些函数中的局部工作变量(字符串、数组等)可能会被调用。可能是问题所在。
我知道更大/更重对象的对象池(我们在一定程度上使用它),但我正在寻找可以全面应用的技术,特别是与在紧密循环中被多次调用的函数有关.
我可以使用哪些技术来减少垃圾收集器必须完成的工作量?
而且,也许还有 - 可以使用哪些技术来识别哪些对象被垃圾收集最多? (这是一个非常大的代码库,因此比较堆的快照并不是很有成效)
在大多数其他情况下,你需要做的很多事情来最小化 GC 流失与被认为是惯用的 JS 相违背,所以在判断我给出的建议时请记住上下文。
分配发生在几个地方的现代口译员中:
当您通过 new 或通过文字语法 [...] 或 {} 创建对象时。当你连接字符串时。当您输入包含函数声明的范围时。当您执行触发异常的操作时。当您评估函数表达式时:(function (...) { ... })。当你执行一个像 Object(myNumber) 或 Number.prototype.toString.call(42) 这样的强制对象的操作时,当你调用一个在后台执行任何这些操作的内置函数时,比如 Array.prototype.slice。当您使用参数来反映参数列表时。当您拆分字符串或使用正则表达式匹配时。
避免这样做,并尽可能集中和重用对象。
具体来说,寻找机会:
将对封闭状态没有或很少依赖的内部函数拉到更高、更长寿的范围内。 (像闭包编译器这样的代码压缩器可以内联内部函数,并且可能会提高您的 GC 性能。)避免使用字符串来表示结构化数据或动态寻址。特别是避免使用拆分或正则表达式匹配重复解析,因为每个都需要多个对象分配。这经常发生在查找表和动态 DOM 节点 ID 的键上。例如,lookupTable['foo-' + x] 和 document.getElementById('foo-' + x) 都涉及分配,因为存在字符串连接。通常,您可以将键附加到长期存在的对象而不是重新连接。根据您需要支持的浏览器,您也许可以使用 Map 直接将对象用作键。避免在正常代码路径上捕获异常。代替 try { op(x) } catch (e) { ... },执行 if (!opCouldFailOn(x)) { op(x); } 别的 { ... }。当您无法避免创建字符串时,例如将消息传递给服务器,请使用像 JSON.stringify 这样的内置函数,它使用内部本机缓冲区来累积内容而不是分配多个对象。避免对高频事件使用回调,并且在可能的情况下,将一个长期存在的函数(参见 1)作为回调传递,该函数从消息内容中重新创建状态。避免使用参数,因为使用参数的函数在调用时必须创建一个类似数组的对象。
我建议使用 JSON.stringify
创建传出网络消息。使用 JSON.parse
解析输入消息显然涉及分配,其中很多用于大消息。如果您可以将传入消息表示为原语数组,那么您可以节省大量分配。您可以围绕它构建不分配的解析器的唯一其他内置函数是 String.prototype.charCodeAt
。一个复杂格式的解析器只使用它,但读起来会很糟糕。
Chrome developer tools 有一个非常好的功能来跟踪内存分配。它被称为记忆时间线。 This article 描述了一些细节。我想这就是你所说的“锯齿”?这是大多数 GC 运行时的正常行为。分配继续进行,直到达到触发收集的使用阈值。通常在不同的阈值有不同种类的集合。
https://i.stack.imgur.com/lwrwY.png
垃圾收集及其持续时间包含在与跟踪关联的事件列表中。在我相当旧的笔记本上,临时收集大约 4Mb,需要 30 毫秒。这是 60Hz 循环迭代中的 2 次。如果这是一个动画,30 毫秒的集合可能会导致卡顿。您应该从这里开始查看您的环境中发生了什么:收集阈值在哪里以及收集需要多长时间。这为您提供了评估优化的参考点。但是您可能不会比通过减慢分配速率来降低卡顿的频率更好,延长收集之间的间隔。
下一步是使用 Profiles |记录堆分配功能可按记录类型生成分配目录。这将快速显示在跟踪期间哪些对象类型消耗的内存最多,这相当于分配率。按速率降序关注这些。
这些技术不是火箭科学。当您可以使用未装箱的物品时,请避免使用装箱的物品。使用全局变量来保存和重用单个装箱对象,而不是在每次迭代中分配新的对象。在空闲列表中汇集公共对象类型,而不是放弃它们。缓存可能在未来迭代中重用的字符串连接结果。通过在封闭范围内设置变量来避免分配只是为了返回函数结果。您必须在自己的上下文中考虑每种对象类型以找到最佳策略。如果您需要有关细节的帮助,请发布描述您正在查看的挑战的详细信息的编辑。
我建议不要在整个应用程序中歪曲您的正常编码风格,以尝试产生更少的垃圾。出于同样的原因,您不应该过早地优化速度。你的大部分努力加上许多额外的复杂性和代码晦涩难懂都将毫无意义。
request animation frame
、animation frame fired
和 composite layers
。我不知道为什么我没有像您一样看到 GC Event
(这是在最新版本的 chrome 上,也是金丝雀)。
@342342
和 code relocation info
。
作为一般原则,您希望尽可能多地缓存,并尽可能少地为循环的每次运行创建和销毁。
我脑海中浮现的第一件事是减少在主循环中使用匿名函数(如果有的话)。此外,很容易陷入创建和销毁传递给其他函数的对象的陷阱。我绝不是 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
});
}
您的程序是否有停机时间?也许您需要它平稳运行一两秒钟(例如动画),然后它有更多的时间来处理?如果是这种情况,我可以看到在整个动画中通常会被垃圾收集的对象,并在某个全局对象中保留对它们的引用。然后当动画结束时,您可以清除所有引用并让垃圾收集器完成它的工作。
抱歉,与您已经尝试和想到的相比,这一切都显得微不足道。
我会在 global scope
中创建一个或几个对象(我确信垃圾收集器不允许触摸它们),然后我会尝试重构我的解决方案以使用这些对象来完成工作,而不是使用局部变量。
当然它不能在代码中的任何地方都完成,但通常这是我避免垃圾收集器的方法。
PS 这可能会使代码的特定部分不太易于维护。
JSON.parse
d 对象分配的空间比消息字符串少(或相等)吗?