Scala 做函数式编程';s的深层堆栈防止JVM中的垃圾收集?

Scala 做函数式编程';s的深层堆栈防止JVM中的垃圾收集?,scala,jvm,Scala,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)之前

假设我分配了一些大对象(例如,大小为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]=。。。

首先,你的问题包含了一个严重的误解,以及一个灾难性的糟糕编码的例子

但是,因为大多数JVM在堆栈展开(AFAIK)之前不会减少引用

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

下一步:

   def f5(vec: Vector[Int]) = { gc(); f6(  vec.map(_+1) ) }
我认为您正试图通过
gc()
调用“强制”垃圾收集。不要这样做:这是非常低效的。即使您只是在调查内存管理行为,也很可能会扭曲该行为,以至于您看到的不是正常Scala代码的代表

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


(我想,另一种方法是Scala编译器显式地将
null
分配给死引用变量。但是,如果没有垃圾保留问题,则可能会出现性能问题!)

首先,您的问题包含一个严重的误解,以及一个灾难性错误编码的示例

但是,因为大多数JVM在堆栈展开(AFAIK)之前不会减少引用

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

下一步:

   def f5(vec: Vector[Int]) = { gc(); f6(  vec.map(_+1) ) }
我认为您正试图通过
gc()
调用“强制”垃圾收集。不要这样做:这是非常低效的。即使您只是在调查内存管理行为,也很可能会扭曲该行为,以至于您看到的不是正常Scala代码的代表

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


(我想,另一种方法是Scala编译器显式地将
null
分配给死引用变量。但是,如果没有垃圾保留问题,这可能会导致性能问题!)

添加到@StephenC的答案中

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

hotspot jit可以在一个方法内进行活动性分析,并认为局部变量即使在框架仍在堆栈上时也是不可访问的。这就是为什么JDK9引入了,在某些情况下,甚至
This
在执行该实例的成员方法时也可能变得不可访问


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

添加到@StephenC的答案中

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

hotspot jit可以在一个方法内进行活动性分析,并认为局部变量即使在框架仍在堆栈上时也是不可访问的。这就是为什么JDK9引入了,在某些情况下,甚至
This
在执行该实例的成员方法时也可能变得不可访问


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

示例中的调用是尾部调用。它们真的不应该分配堆栈帧。但是,对于各种不幸的是,Scala语言规范没有要求正确的尾部调用,同样不幸的是,Scala JVM实现没有执行尾部调用优化

但是,有些JVM具有TCO,例如,J9 JVM执行TCO,因此不应该分配任何额外的堆栈帧,这使得在下一个尾部调用发生时中间对象不可访问。即使没有TCO的JVM也能够执行各种静态(转义分析、活动性分析)或动态分析(逃逸检测,例如Azul Zing JVM执行此操作)可能有助于或可能没有助于此情况的分析

还有其他Scala的实现:据我所知,Scala.js不执行TCO,但它编译为ECMAScript,从ECMAScript 2015开始,ECMAScript确实有适当的尾部调用,因此只要Scala方法调用的编码最终成为ECMAScript函数调用,符合标准的ECMAScript 2015引擎就应该消除TCO卡拉尾巴叫

<