gpt4 book ai didi

java - 为什么这个 Java 方法会泄漏——为什么内联它会修复泄漏?

转载 作者:搜寻专家 更新时间:2023-10-31 08:25:09 25 4
gpt4 key购买 nike

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

给定一个 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()) 调用。所以在我看来,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();
}

最佳答案

在你的方法中

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 变量是 null,但是 lazySeqLocal 包含对第一个元素的引用。

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

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

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

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

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

关于java - 为什么这个 Java 方法会泄漏——为什么内联它会修复泄漏?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52169895/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com