Java 是否有一种习惯用法/模式可以传递集合而不保留对它的引用或阻止进一步使用?
我正在清理一些开始在生产中抛出Java 是否有一种习惯用法/模式可以传递集合而不保留对它的引用或阻止进一步使用?,java,design-patterns,Java,Design Patterns,我正在清理一些开始在生产中抛出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
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!
}
陈述挑战的另一种方式是如何将doSomething()中的内存使用量从三倍减少到两倍,这是一种惯用的方式,可以在编译时强制安全使用?我希望有更好的方法,但我想到了以下几点: 将
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()
是否合适。调用targets.clear()
的问题是(正如您所指出的)可能有其他东西在使用集合。下面是我的方法:
public void doSomething(Collection<Widget> targets) {
// ... do some stuff using TARGETS ...
targets = null;
// ... do some other stuff NOT using TARGETS ...
}
在第一个示例中,JVM应该能够在调用启动后判断调用方没有可访问的引用。在第二个示例中,JVM更难知道。但在这两种情况下,您都依赖于JVM来检测调用方中的引用实际上是不可访问的
更新
我怀疑MemoryTest
示例失败的原因是doSomething
代码正在创建一个临时变量,或者使用寄存器或其他东西来保存对流的引用。JVM可能没有意识到该变量/寄存器不再有效,因此可能会将流
对象视为可访问的。但是流
对象很可能有一个对原始集合的引用,这将使集合也可以访问
可以说这是一个JVM错误,但我不这么认为。JLS和JVM对于JVM是否/何时应该检测到在方法调用中使用的局部变量(或临时变量/寄存器)不再是可访问的,没有做出强有力的声明
但我真的认为波希米亚人给了你最好的答案。(没有。我不认为他是在开玩笑。)
如果您必须对此进行微优化,以将问题压缩到当前(小)堆的内存占用中,那么简单的解决方案是将堆变大
正如您所注意到的,您可以通过各种巧妙的方法来优化存储利用率(例如,通过清除内容),实际上可能会破坏应用程序或使其更难维护
(您的MemoryTest
示例很好地说明了巧妙的优化可能会失败。幕后发生的事情很难预测。)我之所以发布这篇文章,是因为它太大了,无法发表评论:
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);
}
}
它也不应该失败,但它确实失败了。即使使用ZGC
或Shenandoah
。但是,我不知道为什么。如果您对此感到担忧,那么简单地将集合清除为“0”项是否会起作用(例如,您的targets.clear()
)会起作用?它“起作用”,但会触发维护陷阱并导致毫无戒备的调用方中断,无论是现在还是将来,分配更多内存如何?显然,“理想”是在“集合”在范围内时立即初始化它,并在不再需要它时立即构造程序,使其超出范围(因此有资格释放JVM)。但这将涉及重组你的整个应用程序——可能不是一个选项。C++总是有的。在Java.hi@Stephen中是一个相当类似的成语。在方法中将其设置为null
,对GC没有任何帮助,因为调用方中对集合的引用仍然是活动的。事实上,在重新阅读时,我现在想知道这是否应该是正确的
代码的实际行为是否不同。我得测试一下。唉,它没有work@AlexR我不能完全肯定这个例子能证明什么。如果我在main方法中隔离调用方1,比如:publicstaticvoidmain(String[]args){newmemorytest().doSomething(Arrays.asList(newheavyObject(),newheavyObject(),newheavyObject());}
;执行java-Xms6g-Xmx6g MemoryTest.java
它运行得很好。如果我注释inputs=null
,它将失败。正如这个答案所说的那样。@Eugene您介意发布您的实际测试代码吗?我不确定我是否理解。好吧,这是我没有预料到的:你是对的java-Xms6g-Xmx6g MemoryTest.java
与从命令行启动的Oracle jdk-15一起工作。但是,从Eclipse->RunAs Java程序中启动时,使用相同的-Xms6g-Xmx6g参数,指向相同的jdk-15,在OOM中失败!我能看到的唯一区别是Eclipse运行的是javaw
,而不是java
@AlexR。我已经多年没有使用Eclipse了,也无法测试它,但命令行对我来说是事实。总是。将参数设置为null
以帮助GC仍然在JDK代码中使用,请查看LinkedList
和LinkedBlockingQueue
。
// This should be OK
doSomething(computeWidgets(...));
// This may be a problem
Collection<Widget> targets = computeWidgets(...);
doSomething(targets);
// Don't use 'targets' from now on.
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);
}
}
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);
}