是否有一种习惯用法/模式可以在不保留对集合的引用或阻止进一步使用的情况下传递集合



我正在清理一些在生产中开始抛出java.lang.OutOfMemoryError的代码。

有问题的领域有几种处理大型藏品的方法,例如:

public void doSomething(Collection<HeavyObject> inputs) {
... do some stuff using INPUTS, deriving some different objects ...
... do some other stuff NOT using INPUTS, only derived objects ...
}
public void unsuspectingCaller() {
Collection<HeavyObject> largeCollection;
... some stuff to populate the collection ...
doSomething(largeCollection);
... other stuff ...
// this following code may be added in the future
kaboom(largeCollection); // walks into maintenance trap!
}

... do some other stuff NOT using INPUTS ...中的代码正在爆炸并耗尽内存

我可以通过在两个块之间添加inputs.clear()来减少内存消耗(允许早期GC(。

但是,我不想为未来的维护人员设置陷阱,他们可能不知道输入集合已被清除。事实上,inputs在理想情况下是不可变的,以便更清楚地传达代码的意图。

有没有一种惯用的方法来声明doSomething(),以明确甚至编译器可验证doSomething()的调用方在调用doSomething()后不应该继续使用集合?

更新

为了更清楚起见,我将参数重命名为inputs,而不是targets。在查看评论时请记住这一点。

更新2

根据@Stephen C的建议,我们可以清楚地看到JVM不会释放调用方持有的引用,即使它们只是作为未命名的参数传入的。使用-Xmx8g(失败(和-Xmx9g(通过(执行:

package com.stackoverflow.sandbox;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
public class MemoryTest {
static class HeavyObject {
int[] oneGigabyte = IntStream.range(0, 256_000_000).toArray();
public int[] getGig() {
return oneGigabyte;
}
}

private int[] skynet(int[] in) {
// perform out-of-this-world artificial intelligence computation
return Arrays.stream(in).map(x -> x >> 1).toArray();
}

void doSomething(Collection<HeavyObject> input) {
Collection<int[]> doubleMemoryUsage = input.stream().map(HeavyObject::getGig).map(this::skynet).collect(Collectors.toList());
input = null;

Collection<int[]> tripleMemoryUsage = doubleMemoryUsage.stream().map(this::skynet).collect(Collectors.toList());

double sum = tripleMemoryUsage.stream().flatMapToDouble(array -> Arrays.stream(array).asDoubleStream()).sum();
System.out.println("sum = " + sum);
}

@Test
void caller1() {
doSomething(List.of(new HeavyObject(), new HeavyObject(), new HeavyObject()));
System.out.println("done1");
}

@Test
void caller2() {
Collection<HeavyObject> threeGigs = List.of(new HeavyObject(), new HeavyObject(), new HeavyObject());
doSomething(threeGigs);
System.out.println("done2");
}
}

另一种表述挑战的方法是,如何以惯用的方式将doSomething((中的内存使用量从三倍减少到两倍,从而在编译时强制安全使用

我之所以发布这个,是因为它太大了,无法发表评论:

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class MemoryTest {
public static void main(String[] args) {
new MemoryTest().doSomething(List.of(new HeavyObject(), new HeavyObject(), new HeavyObject()));
}

static class HeavyObject {
int[] oneGigabyte = IntStream.range(0, 256_000_000).toArray();
public int[] getGig() {
return oneGigabyte;
}
}
private int[] skynet(int[] in) {
// perform out-of-this-world artificial intelligence computation
return Arrays.stream(in)
.map(x -> x >> 1)
.toArray();
}

void doSomething(Collection<HeavyObject> input) {
Collection<int[]> doubleMemoryUsage = input.stream().map(HeavyObject::getGig).map(this::skynet).collect(Collectors
.toList());
input = null;
Collection<int[]> tripleMemoryUsage = doubleMemoryUsage.stream().map(this::skynet).collect(Collectors.toList());
double sum = tripleMemoryUsage.stream().flatMapToDouble(array -> Arrays.stream(array).asDoubleStream()).sum();
System.out.println("sum = " + sum);
}
}

并使用java -Xms6g -Xmx6g MemoryTest.java运行此操作。它会起作用的。

现在注释input = null;并运行它:它将失败。

jdk-15btw.上运行此程序


理论上,即使您将方法更改为:

void doSomething() {
Collection<HeavyObject> input = List.of(new HeavyObject(), new HeavyObject(), new HeavyObject());
Collection<int[]> doubleMemoryUsage = input.stream().map(HeavyObject::getGig).map(this::skynet).collect(Collectors
.toList());
//input = null;
Collection<int[]> tripleMemoryUsage = doubleMemoryUsage.stream().map(this::skynet).collect(Collectors.toList());
double sum = tripleMemoryUsage.stream().flatMapToDouble(array -> Arrays.stream(array).asDoubleStream()).sum();
System.out.println("sum = " + sum);
}

它也不应该失败,但它确实失败了。即使使用CCD_ 15或CCD_。不过,我不知道为什么。

调用targets.clear()的问题是(正如您所指出的(其他人可能正在使用该集合。以下是我的处理方法:

public void doSomething(Collection<Widget> targets) {
// ... do some stuff using TARGETS ...
targets = null;
// ... do some other stuff NOT using TARGETS ...
}

targets = null;将阻止调用保留对集合的引用的时间超过其需要的时间。性能影响几乎为零,过早置零的损坏(如果有的话!(局限于doSomething()方法本身。

然后问题落在了调用者身上:

// This should be OK
doSomething(computeWidgets(...));
// This may be a problem
Collection<Widget> targets = computeWidgets(...);
doSomething(targets);
// Don't use 'targets' from now on.

在第一个示例中,JVM应该能够在调用开始后告知调用者没有可访问的引用。在第二个例子中,JVM更难知道。但在这两种情况下,您都依赖JVM来检测引用是否在调用者中有效地不可访问。

更新

我怀疑MemoryTest示例失败的原因是doSomething代码正在创建一个临时变量,或者使用寄存器或其他东西来保存对Stream对象的引用。JVM可能没有意识到变量/寄存器不再有效,因此可能将Stream对象视为可访问。但是Stream对象很可能具有对原始集合的引用,这也将使集合可访问。

这可能是JVM的一个错误,但我不这么认为。JLS和JVMS没有对JVM是否/何时应该检测到使用方法调用的本地变量(或临时变量/寄存器(不再可访问做出强有力的声明。


但我真的认为《波西米亚人》给了你最好的答案。(不。我不认为他在开玩笑。(

如果您必须对此进行微优化,以将您的问题压缩到当前(小(堆的内存占用中,那么简单的解决方案就是使堆更大。

正如您所指出的,您可以做各种巧妙的事情来优化存储使用率(例如通过清理东西(,这些事情实际上可能会破坏应用程序或使其更难维护。

(你的MemoryTest例子很好地说明了聪明的优化可能会失败。幕后发生的事情很难预测。(

我本来希望能有更好的结果,但我想到了:

doSomething()重构为一个类。

class DoerOfSomething {
public DoerOfSomething(Collection<HeavyObject> inputs) {
... do some stuff using INPUTS, deriving other objects ...
// derived objects are set as members
// inputs goes out of scope
} 
public void doSomething() {
... do some other stuff NOT using INPUTS, only derived objects ...
}
}

现在,调用者可以执行自己的分析,看看targets.clear()是否可以调用。

最新更新