首页 > 解决方案 > 为什么这个 Java 方法会泄漏——为什么内联它会修复泄漏?

问题描述

我写了一个最小的有些惰性(int)序列类,GarbageTest.java,作为一个实验,看看我是否可以在 Java 中处理非常长的惰性序列,就像在 Clojure 中一样。

给定一个naturals()返回惰性、无限、自然数序列的方法;drop(n,sequence)删除 的第一个n元素sequence并返回其余部分的方法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()然后在调用中调用 IntelliJ 的“内联”重构来生成的nth(N, naturals())。所以在我看来, 的行为dropTest()应该与 的行为相同nthTest()

但并不完全相同!dropTest()以 N 高达 1e8 运行完成,而 N 小至 1e6 则nthTest()失败OutOfMemoryError

我避免了内部课程。而且我已经尝试了我的代码的变体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();
}

标签: javamemory-managementclojuregarbage-collectionsequence

解决方案


在你的方法中

static int nth(final int n, final LazyishSeq lazySeq) {
    return drop(n, lazySeq).head();
}

参数变量lazySeq在整个drop操作期间保存对序列第一个元素的引用。这可以防止整个序列被垃圾收集。

与...对比

public void dropTest() {
    assertThat( drop(N, naturals()).head(), is(N+1));
}

序列的第一个元素由naturals()的调用返回并直接传递给drop,因此从操作数堆栈中删除并且在 的执行期间不存在drop

您尝试将参数变量设置为null,即

static int nth(final int n, /*final*/ LazyishSeq lazySeqArg) {
    final LazyishSeq lazySeqLocal = lazySeqArg;
    lazySeqArg = null;
    return drop(n,lazySeqLocal).head();
}

没有帮助,就像现在一样,lazySeqArg变量 is null,但lazySeqLocal保存对第一个元素的引用。

局部变量通常不会阻止垃圾收集,允许收集其他未使用的对象,但这并不意味着特定实现能够做到这一点。

在 HotSpot JVM 的情况下,只有优化的代码才能摆脱这些未使用的引用。但这里,nth并不是热点,因为重重的事情发生在drop方法之内。

这就是为什么该drop方法没有出现相同问题的原因,尽管它还包含对其参数变量中第一个元素的引用。该drop方法包含执行实际工作的循环,因此很可能会被 JVM 优化,这可能会导致它消除未使用的变量,从而允许收集已处理的序列部分。

有许多因素可能会影响 JVM 的优化。除了代码的不同形状之外,似乎在未优化阶段快速分配内存也可能会降低优化器的改进。确实,当我使用-Xcompile, 完全禁止解释执行时,两个变体都成功运行,甚至int N = (int)1e9不再有问题。当然,强制编译会增加启动时间。

我不得不承认我不明白为什么混合模式的表现差很多,我会进一步调查。但通常,您必须意识到垃圾收集器的效率取决于实现,因此在一个环境中收集的对象可能会留在另一个环境中的内存中。


推荐阅读