【发布时间】:2019-02-09 16:47:52
【问题描述】:
我编写了一个最小的有点惰性 (int) 序列类 GarbageTest.java,作为一个实验,看看我是否可以像在 Clojure 中那样在 Java 中处理非常长的惰性序列。
给定一个naturals() 方法,它返回惰性的、无限的自然数序列;一个drop(n,sequence) 方法,它删除sequence 的第一个n 元素并返回sequence 的其余部分;和一个简单返回的nth(n,sequence) 方法:drop(n, lazySeq).head(),我写了两个测试:
static int N = (int)1e6;
// succeeds @ N = (int)1e8 with java -Xmx10m
@Test
public void dropTest() {
assertThat( drop(N, naturals()).head(), is(N+1));
}
// fails with OutOfMemoryError @ N = (int)1e6 with java -Xmx10m
@Test
public void nthTest() {
assertThat( nth(N, naturals()), is(N+1));
}
请注意,dropTest() 的主体是通过复制 nthTest() 的主体然后在 nth(N, naturals()) 调用上调用 IntelliJ 的“内联”重构来生成的。所以在我看来dropTest() 的行为应该与nthTest() 的行为相同。
但它并不相同! dropTest() 运行完成,N 最大为 1e8,而 nthTest() 失败,OutOfMemoryError N 小至 1e6。
我避免使用内部类。我已经尝试了我的代码的变体ClearingArgsGarbageTest.java,它在调用其他方法之前将方法参数设为空。我已经应用了 YourKit 分析器。我看过字节码。我只是找不到导致nthTest() 失败的泄漏。
“泄漏”在哪里?为什么nthTest() 有泄漏而dropTest() 没有?
这里是来自GarbageTest.java 的其余代码,以防您不想点击进入 Github 项目:
/**
* a not-perfectly-lazy lazy sequence of ints. see LazierGarbageTest for a lazier one
*/
static class LazyishSeq {
final int head;
volatile Supplier<LazyishSeq> tailThunk;
LazyishSeq tailValue;
LazyishSeq(final int head, final Supplier<LazyishSeq> tailThunk) {
this.head = head;
this.tailThunk = tailThunk;
tailValue = null;
}
int head() {
return head;
}
LazyishSeq tail() {
if (null != tailThunk)
synchronized(this) {
if (null != tailThunk) {
tailValue = tailThunk.get();
tailThunk = null;
}
}
return tailValue;
}
}
static class Incrementing implements Supplier<LazyishSeq> {
final int seed;
private Incrementing(final int seed) { this.seed = seed;}
public static LazyishSeq createSequence(final int n) {
return new LazyishSeq( n, new Incrementing(n+1));
}
@Override
public LazyishSeq get() {
return createSequence(seed);
}
}
static LazyishSeq naturals() {
return Incrementing.createSequence(1);
}
static LazyishSeq drop(
final int n,
final LazyishSeq lazySeqArg) {
LazyishSeq lazySeq = lazySeqArg;
for( int i = n; i > 0 && null != lazySeq; i -= 1) {
lazySeq = lazySeq.tail();
}
return lazySeq;
}
static int nth(final int n, final LazyishSeq lazySeq) {
return drop(n, lazySeq).head();
}
【问题讨论】:
-
如果您使用 Java,使用 Streams 不是更自然吗?
-
@AlanThompson 总的来说你是对的。流是“Java 方式”。我这样做是为了尝试以 Haskell/Clojure 方式做事:不可变、惰性、可扩展的序列。更多信息请参见:github.com/vavr-io/vavr/issues/2245#issuecomment-390437457
-
实验通常很有帮助,但您不想花太多时间“重新发明轮子”。如果您还没有查看,您可能应该查看 Clojure LazySeq 源代码:github.com/clojure/clojure/blob/…
-
我实验的唯一目的是看看是否可以将 Clojure 风格的不可变、惰性、可扩展序列实现为 Java 库。我的结论是这是不可能的,像 vavr.io 这样的 Java 库将无法做到这一点。我相信,如果我采用 Clojure 序列实现类并从我自己的 Java 代码中调用它们,那么该代码将表现出与我的示例相同的内存消耗——除非我为 JVM 提供了
-Xcomp选项。 -
您完全可以在不设置任何 JVM 选项的情况下调用 Clojure 类而不会泄漏内存——可能只需要像 Clojure 那样“清除本地变量”。添加了一个答案来解释如何在 Clojure 运行时的 Java 部分完成。
标签: java memory-management clojure garbage-collection sequence