在JavaScript中减less垃圾收集器活动的最佳实践

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

所以,我正在努力研究减less垃圾收集器的工作量的最佳实践。 (我在网上find的大部分信息都是关于避免内存泄漏,这是一个稍微不同的问题 – 我的内存正在被释放,只是垃圾收集太多了。)我假设这主要归结为尽可能地重复使用对象,但当然魔鬼是在细节中。

该应用程序是按照John Resig的简单JavaScriptinheritance的 “类”构造的。

我认为一个问题是一些函数每秒可以调用几千次(因为它们在主循环的每次迭代中使用了几百次),也许这些函数中的局部工作variables(string,数组等等)可能是问题。

我知道更大/更重的对象的对象池(我们在一定程度上使用这个对象),但是我正在寻找可以在整个板上应用的技术,特别是在紧密循环中被称为非常多次的函数。

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

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

在大多数情况下,为了最大限度地减lessGCstream失,你需要做的很多事情都是违背惯用的JS,所以在判断我给出的build议时请记住上下文。

现代口译员在几个地方进行分配:

  1. 当您通过new或通过文字语法[...]{}创build对象时。
  2. 当你连接string。
  3. 当您input包含函数声明的作用域时。
  4. 当您执行触发exception的操作时。
  5. 当你评估一个函数expression式: (function (...) { ... })
  6. 当你执行一个Number.prototype.toString.call(42) Object(myNumber)的操作,如Object(myNumber)或者Number.prototype.toString.call(42)
  7. 当你调用一个内置的引擎时,就像Array.prototype.slice
  8. 当您使用arguments来反映参数列表。
  9. 分割string或与正则expression式匹配时。

避免做到这些,尽可能地集中和重用对象。

具体来说就是寻找机会:

  1. 将那些没有或很less依赖closures状态的内部函数拉入更高,更长的范围。 (像Closure编译器这样的代码缩减器可以内联内部函数,并可能提高GC的性能。)
  2. 避免使用string来表示结构化数据或dynamic寻址。 特别是避免使用split或正则expression式匹配反复parsing,因为每个对象都需要多个对象分配。 这经常发生在查找表和dynamicDOM节点ID的键中。 例如, lookupTable['foo-' + x]document.getElementById('foo-' + x)都涉及一个分配,因为有一个string连接。 通常,您可以将键连接到长时间的对象而不是重新连接。 根据您需要支持的浏览器,您可以使用Map直接使用对象作为关键字。
  3. 避免捕获正常代码path上的exception。 try { op(x) } catch (e) { ... } ,而不是try { op(x) } catch (e) { ... } if (!opCouldFailOn(x)) { op(x); } else { ... } if (!opCouldFailOn(x)) { op(x); } else { ... }
  4. 当你不能避免创buildstring,例如传递消息到服务器,使用一个内置的JSON.stringify ,它使用内部本地缓冲区来累积内容而不是分配多个对象。
  5. 避免对高频事件使用callback函数,并且在可能的情况下,将callback函数作为一个长寿函数(参见1)从消息内容中重新创build状态。
  6. 避免使用arguments因为函数调用时必须创build一个类似数组的对象。

我build议使用JSON.stringify创build传出networking消息。 使用JSON.parseparsinginput消息显然涉及分配,并且大量消息用于大型消息。 如果您可以将传入的消息表示为基元数组,则可以节省大量的分配。 您可以构build一个不分配的parsing器的唯一的其他内置是String.prototype.charCodeAt 。 一个复杂的格式parsing器,只使用这将是地狱般的阅读。

作为一个普遍的原则,你想要尽可能的caching,尽可能less的创build和销毁你的循环的每一个运行。

我脑子里首先想到的是在主循环内部减less使用匿名函数(如果有的话)。 而且,陷入创build和销毁传递给其他函数的对象的陷阱也很容易。 我绝不是一个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 }); } 

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

对不起,如果这是相对于你已经尝试和想到的一点点微不足道。

Chrome开发者工具有一个非常好的function来跟踪内存分配。 这就是所谓的内存时间线。 本文介绍一些细节。 我想这就是你所说的“锯齿”? 这是大多数GC'ed运行时的正常行为。 分配继续进行,直到达到使用阈值,触发收集。 通常在不同的门槛上有不同种类的collections。

内存时间轴在Chrome中

与跟踪关联的事件列表中包含垃圾收集及其持续时间。 在我的相当老的笔记本上,短暂的收集发生在4Mb左右,需要30ms。 这是60Hz循环迭代中的2个。 如果这是一个animation,30ms的集合可能导致口吃。 你应该从这里开始,看看你的环境中发生了什么:收集的门槛是多less,收集的时间是多less。 这给你一个评估优化的参考点。 但是,通过减缓分配速度,延长收集间隔,你可能不会比减less口吃的频率做得更好。

下一步是使用Profiles | logging堆分配function可以按loggingtypes生成分配目录。 这将很快显示哪些对象types在跟踪期间消耗的内存最多,这相当于分配率。 把重点放在这个比率的降序上。

技术不是火箭科学。 避免使用盒装对象时,可以使用无盒装的对象。 使用全局variables来保存和重用单个盒装对象,而不是在每次迭代中分配新的盒装对象。 将空闲列表中的公共对象types池化,而不是放弃它们。 cachingstring连接结果可能在未来的迭代中可重用。 避免分配只是为了通过在封闭范围内设置variables来返回函数结果。 您将不得不考虑每个对象types在自己的上下文中find最佳策略。 如果您需要特定帮助,请发布编辑,描述您正在查看的挑战的详细信息。

我build议不要在一个应用程序中用霰弹枪来破坏你的正常编程风格,以减less垃圾。 这是出于同样的原因,你不应该过早地优化速度。 你的大部分努力加上代码中增加的复杂性和模糊性将是没有意义的。

我会在global scope创build一个或几个对象(我确信垃圾收集器不允许触摸它们),然后尝试重构我的解决scheme以使用这些对象完成工作,而不是使用局部variables。

当然,在代码中无处不在,但通常这是我的方式来避免垃圾回收。

PS这可能会使代码的特定部分less一点维护。