函数式编程的深度堆栈是否阻止了 JVM 中的垃圾回收?



假设我分配了一些大对象(例如,一个大小为N的向量,它可能非常大(,并对其执行一系列m个操作:

fm( .. f3( f2( f1( vec ) ) ) )

每个返回一个大小为N.的集合

为了简单起见,我们假设每个f都是非常简单的

def f5(vec: Vector[Int]) = { gc(); f6(  vec.map(_+1) ) }

因此,vec在进行每个后续调用时不再具有未来引用。(在输入f2之后,f1的vec参数永远不会被使用,依此类推,每次调用(

然而,因为大多数JVM在堆栈展开(AFAIK(之前不会减少引用,所以我的程序不需要来消耗NxM内存吗。相比之下,在以下样式中只需要2xM(在其他实现中更少(

  var vec:Vector[Int] = ...
  for ( f <- F ) {
    vec = f(vec)
    gc()
  }

尾递归方法是否存在同样的问题?

这不仅仅是一个学术练习——在某些类型的大数据类型问题中,我们可能会选择N,以便我们的程序完全适合RAM。在这种情况下,我是否应该担心一种类型的流水线比另一种更可取?

首先,您的问题包含一个严重的误解,以及一个糟糕编码的例子。

然而,因为大多数JVM在堆栈展开(AFAIK(之前不会减少引用。。。

实际上,根本没有主流JVM在引用上使用引用计数。相反,它们都使用不依赖引用计数的标记扫描、复制或代收集算法。

下一篇:

   def f5(vec: Vector[Int]) = { gc(); f6(  vec.map(_+1) ) }

我认为您正试图使用gc()调用来"强制"垃圾回收。不要这样做:效率非常低。即使您只是为了研究内存管理行为,您也很可能会扭曲这种行为,以至于您所看到的并不能代表正常的Scala代码。

话虽如此,答案基本上是肯定的。如果您的Scala函数不能进行尾调用优化,那么深度递归可能会导致垃圾保留问题。唯一的"退出"是JIT编译器能够告诉GC某些变量在方法调用的特定点上"死了"。我不知道HotSpot JIT/GC是否能做到这一点。

(我想,另一种方法是Scala编译器显式地将null分配给死引用变量。但当你没有垃圾保留问题时,这会带来潜在的性能问题!(

添加到@StephenC的答案

我不知道HotSpot JIT/GC是否能做到这一点。

热点jit可以在方法中进行活跃度分析,并认为即使帧仍在堆栈上,局部变量也是不可访问的。这就是为什么JDK9引入了Reference.areachabilityFence,在某些情况下,即使是this在执行该实例的成员方法时也可能变得不可访问。

但这种优化只适用于控制流中确实没有任何东西仍然可以读取该局部变量的情况,例如没有finally块或监视器退出。因此,它将取决于scala生成的字节码。

示例中的调用是尾部调用。他们真的不应该有一个堆栈帧分配在所有。然而,由于各种不幸的原因,Scala语言规范没有强制执行适当的尾调用,同样不幸的原因是,ScalaJVM实现没有执行尾调用优化。

然而,有些JVM具有TCO,例如J9 JVM执行TCO,因此不应该分配任何额外的堆栈帧,使得下一个尾部调用一发生,中间对象就无法访问。即使没有TCO的JVM也能够执行各种静态(转义分析、活跃度分析(或动态(转义检测,例如Azul Zing JVM这样做(分析,这些分析可能对这种情况有帮助,也可能没有帮助。

Scala还有其他实现:据我所知,Scala.js不执行TCO,但它编译到ECMAScript,并且截至ECMAScript2015,ECMAScript确实有适当的尾部调用,所以只要Scala方法调用的编码最终成为ECMAScript函数调用,符合标准的ECMAScript2015engine就应该消除Scala尾部调用。

Scala本机目前不执行TCO,但将来会执行。

相关内容

  • 没有找到相关文章