好的,让我看看是否可以解释。
我有一些代码将Java迭代器(碰巧来自Hadoop)封装在Scala流中,这样我就可以通过无法直接控制的客户端代码多次读取它。这个Stream最后要做的就是reduce()操作。Stream会记住它已经看到的所有项目。不幸的是,在某些情况下,迭代器会非常大,因此将所有项目存储在其中会导致内存不足错误。然而,一般来说,客户端代码需要多次迭代功能的情况与破坏内存的迭代程序不同,如果确实存在这种情况,那就不是我的问题。
我想确保的是,我可以为需要它的代码提供记忆功能,但不能为不需要它的程序提供记忆功能(尤其是那些根本不看Stream的程序)。
Stream中reduce()的代码表示,它的编写方式允许在减少时对Stream中已经访问过的部分进行GC。所以,如果我能确保这真的发生,我会没事的。但在实践中,我如何才能确保这种情况发生?特别是,如果函数A创建流并将其传递给函数B,函数B将流传递给函数C,然后函数C调用reduce(),那么函数A、B和C中对流的引用呢?在所有这些情况下,三个函数中的任何一个都不会进一步使用流,尽管调用不一定是尾递归的。JVM是否足够聪明,可以确保在调用reduce()时,其来自函数A、B和C的引用计数为0,从而可以进行GC?本质上,这意味着JVM在函数A中注意到,它对项所做的最后一件事是调用函数B,因此它在调用B的同时消除了自己的句柄,同样地,B到C,C到reduce()。
如果这能正常工作,那么如果A、B或C有一个局部变量保持在项上,它也能工作吗?(再说一遍,以后就不会使用了。)这是因为在不使用本地变量的情况下正确地进行编码会更棘手。
一个在作用域中但永远不会从中读取的变量是死的。JVM可以出于垃圾收集的目的随意忽略死变量;仅由死变量指向的对象是不可访问的,并且可以被收集。JLS的相关部分是,相当模糊的,§12.6.1实施定稿,其中写道:
可达对象是指可以在任何潜在的持续计算中从任何活动线程访问的任何对象。
并解释道:
程序的优化转换可以设计为将可访问的对象数量减少到少于那些天真地认为可访问的。例如,Java编译器或代码生成器可以选择将不再使用的变量或参数设置为null,以使此类对象的存储可以更快地回收。
另一个例子是,如果对象字段中的值存储在寄存器中,就会发生这种情况。然后,程序可以访问寄存器而不是对象,并且再也不会访问对象。这意味着该对象是垃圾。请注意,只有当引用在堆栈上,而不是存储在堆中时,才允许进行这种优化。
如果方法A只有引用流的死变量,那么它不会阻碍其集合。
然而,请注意,这意味着局部变量:如果您有引用流的字段(包括来自封闭嵌套类的方法的封闭局部变量),那么这不适用;我不认为JVM被允许将这些视为死的。换句话说,这里:
public Callable<String> foo(final Object o) {
return new Callable<String>() {
public String call() throws InterruptedException {
String s = o.toString();
Thread.sleep(1000000);
return s;
}
};
}
在收集匿名Callable
之前,无法收集对象o
,即使在toString
调用之后从未使用过它,因为在Callable
中有一个引用它的合成字段。