使用Node.js进行垃圾收集

32

我对node.js中嵌套函数的模式如何与v8的垃圾收集器配合工作感到好奇。这里有一个简单的例子:

readfile("blah", function(str) {
   var val = getvaluefromstr(str);
   function restofprogram(val2) { ... } (val)
})

如果restofprogram是长时间运行的,那么这是否意味着str永远不会被垃圾回收?我的理解是,在node中,您经常会得到嵌套函数。如果restofprogram在外部声明,以便str无法在范围内,那么这些函数是否被垃圾回收了?这是一个推荐的做法吗?

编辑 我并没有打算让问题变得复杂。那只是疏忽,所以我已经修改了它。


我相信V8的垃圾回收机制非常智能。如果您将其设置为null作为额外措施,它会被GBed吗? - Alfred
我也希望将其置空(null),以便收集它。然而,该符号可能仍会占用符号表中的空间。 - dhruvbird
相关问题及其良好的答案:JavaScript 中闭包和作用域在运行时是如何表示的(附带更好的代码示例),关于闭包、词法环境和垃圾回收(附有漂亮的作用域检查器截图)。 - Bergi
3个回答

73

简单回答:如果str的值没有被其他地方引用(并且str本身没有被restofprogram引用),那么当function (str) { ... }返回时,它将变得无法访问。

细节:V8编译器区分真正的局部变量和由闭包捕获的所谓上下文变量,受到with语句或eval调用的影响而被遮蔽。

局部变量存在于堆栈中,并在函数执行完成后消失。

上下文变量存在于堆分配的上下文结构中。当上下文结构死亡时,它们就会消失。这里需要注意的重要一点是,来自同一作用域的上下文变量存在于相同的结构中。让我用一个示例代码来说明:

function outer () {
  var x; // real local variable
  var y; // context variable, referenced by inner1
  var z; // context variable, referenced by inner2

  function inner1 () {
    // references context 
    use(y);
  }

  function inner2 () {
    // references context 
    use(z);
  }

  function inner3 () { /* I am empty but I still capture context implicitly */ } 

  return [inner1, inner2, inner3];
}
在这个例子中,变量x会在outer返回时立即消失,但是变量yz只有在inner1inner2inner3三个闭包都不存在时才会消失。这是因为yz被分配在同一个上下文结构中,所有三个闭包隐式地引用了这个上下文结构(即使inner3没有显式使用它)。
当你开始使用with语句、try/catch语句(在V8中包含catch子句中的隐式with语句)或全局eval时,情况会变得更加复杂。
function complication () {
  var x; // context variable

  function inner () { /* I am empty but I still capture context implicitly */ }

  try { } catch (e) { /* contains implicit with-statement */ }

  return inner;
}
在这个例子中,只有当 inner 消失时,x 才会消失。因为:
  • try/catch - 在 catch 子句中包含隐式的with语句
  • V8 假设任何 with 语句都会遮盖所有本地变量
这迫使 x 成为一个上下文变量,并且 inner 捕获了上下文,因此 x 存在直到 inner 结束。
通常,如果您想确保给定变量不会保留一些对象的时间比实际需要的时间更长,您可以通过将该变量的值分配为 null 来轻松 “破坏” 这个链接。

在2023年,是否有人能够评论关于try/catch中是否还包含了隐式的with语句的部分是否仍然相关?规范的当前版本说明catchEnv应该是一个新的声明性环境记录,它没有提到with语句中的对象环境。这只是早期v8实现中的一种奇怪临时解决方法吗? - Andrey Tyukin
有人能否评论一下,在2023年,关于try/catch中是否仍然包含隐式的with语句这一部分是否仍然相关?当前规范版本说明了catchEnv应该是一个新的声明性环境记录,它对于with语句中的对象环境没有任何说明。这只是v8早期实现中的一种奇怪的临时解决方法吗? - undefined

5
实际上,您的示例有些棘手。这是故意的吗?您似乎使用内部词法作用域的restofprogram()函数的val参数掩盖了外部val变量,而不是实际使用它。但无论如何,您正在询问str,因此为了简单起见,让我忽略您的示例中val的棘手性。
我猜测,即使restofprogram()不使用str,该变量也不会在函数完成之前被收集。如果restofprogram()不使用eval()和new Function(),那么它可能会被安全地收集,但我怀疑不会这样做。这对于V8来说是一个棘手的优化,可能不值得麻烦。如果语言中没有eval和new Function(),那么就会容易得多。
现在,这并不意味着它永远不会被收集,因为单线程事件循环中的任何事件处理程序都应该几乎立即完成。否则,整个进程将被阻塞,您将面临比内存中一个无用的变量更大的问题。
现在,我想知道您是否没有在实际编写示例时表达出您实际想要的东西。Node中的整个程序与浏览器中的程序完全相同-它只是注册异步触发的事件回调函数,这些函数在主程序体已经完成后稍后异步触发。此外,没有任何处理程序会阻塞,因此实际上没有任何函数需要花费任何显着的时间来完成。我不确定我是否理解了您实际上在问题中想要表达的意思,但我希望我所写的内容对于理解它们是如何工作的有所帮助。
更新:
在评论中阅读更多关于您的程序外观的信息之后,我可以说得更多。
如果您的程序类似于:
readfile("blah", function (str) {
  var val = getvaluefromstr(str);
  // do something with val
  Server.start(function (request) {
    // do something
  });
});

那么你也可以这样写:
readfile("blah", function (str) {
  var val = getvaluefromstr(str);
  // do something with val
  Server.start(serverCallback);
});
function serverCallback(request) {
  // do something
});

在调用Server.start()后,str将会超出作用域并最终被回收。此外,这将使您的缩进更加易于管理,在更复杂的程序中不应低估其重要性。
至于val,在这种情况下,您可以将其设置为全局变量,这将大大简化您的代码。当然,您也可以使用闭包,但在这种情况下,使val成为全局变量或使其存在于适用于readfile回调和serverCallback函数的外部作用域似乎是最简单的解决方案。
请记住,任何您可以使用匿名函数的地方,您也可以使用命名函数,并且对于这些函数,您可以选择它们所在的作用域。

是的,但如果restofprogram类似于Server.start(function(request){dosomething}),即使restofprogram立即退出,传递给Server.start的函数仍将永久存在,并且具有str作用域。 - Vishnu
实际上,事件处理程序可以创建一个匿名函数,将其添加为其他事件的事件侦听器,并且每次调用它时都可以这样做,从而确保所有作用域变量(对于此处理程序的所有调用)都不会被收集。 - dhruvbird
@dhruvbird:没错。对于这些情况,我建议使用命名函数,您可以选择它们所在的作用域。 - rsp
@Vishnu:请查看我的答案更新,了解如何更好地处理这种情况的一些想法。 - rsp
谢谢,那正是我问题的意图。因此,可能会发生意外的内存泄漏,使用命名函数尽可能减轻这个问题。 - Vishnu

1

我猜测str不会被垃圾回收,因为它可以被restofprogram()使用。 是的,如果restofprogram是在外部声明的话,str应该会被垃圾回收,除非你做了类似这样的事情:

function restofprogram(val) { ... }

readfile("blah", function(str) {
  var val = getvaluefromstr(str);
  restofprogram(val, str);
});

或者,如果getvaluefromstr被声明为以下这样的东西:

function getvaluefromstr(str) {
  return {
    orig: str, 
    some_funky_stuff: 23
  };
}

跟进问题:v8 是仅进行普通 GC 还是同时进行 GC 和引用计数(例如 Python)的混合方式?

V8使用分代垃圾回收器。 - rsp
@MooGoo 我怀疑任何垃圾回收机制都无法智能地检测到在 eval 中使用的 "str"(因为要 eval 的字符串可能来自用户输入)。 - dhruvbird
如果函数体中有evalnew Function语句,那么str可能会被使用,因此不会被垃圾回收。如果没有,在函数体中也不存在对str的直接引用,那么它就可以被垃圾回收。实际上很简单,但它是否是处理器时间的有效利用则是另一个问题... - MooGoo
@MooGoo eval的规则非常复杂,所以我不记得它们的确切内容,但我猜外部实体可以通过作用域或参数将句柄传递给函数内的eval,然后你就可以在函数内部使用eval。(关于使用其他别名调用的eval的作用域规则,我不确定,所以不要引用我)。 - dhruvbird
j=44; e = { foo: some_custom_function }; function foo() { var j = 10; eval = e.foo; eval("j=20"); }; foo(); 现在,如果some_custom_function实际上是全局eval,则全局j将保持不变。但是,如果它是一些有趣的函数,打印“hello”并且没有副作用,那么node将不必要地保持范围变量处于活动状态。 - dhruvbird
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接