在Java中,变量应该在循环内部还是外部声明?

3

我知道之前已经有人问过类似的问题,但我仍然不确定对象何时变得可回收,以及哪种方法更有效。

方法一:

for (Item item : items) {
    MyObject myObject = new MyObject();
    //use myObject.
}

第二种方法:

MyObject myObject = null;
for (Item item : items) {
    myObject = new MyObject();
    //use myObject.
}

我理解:通过减少局部变量的范围,可以提高代码的可读性和可维护性,并降低错误的可能性。(Joshua Bloch)。
但是内存消耗和性能如何呢?在Java中,当没有对对象的引用时,对象将被垃圾回收。如果有100000个项目,则将创建100000个对象。在第一种方法中,每个对象都将有一个引用(myObject),因此它们不符合GC的条件?
而在第二种方法中,每次循环迭代时,您都会从先前迭代中创建的对象中删除引用。因此,在第一次循环迭代之后,对象肯定开始变得符合GC的条件。
还是说这是性能和代码可读性和可维护性之间的权衡?
我误解了什么吗?
注意: 假设我关心性能并且在循环之后不需要myObject。
提前感谢。

性能可能是一个问题,但是编写正确的代码并将变量声明放在循环内部要好得多。然后再测量性能。 在确保需要优化之前,永远不要“优化”任何东西。其他任何方法都是疯狂的。 - markspace
我认为GC聪明到足以在需要时清除方法二中每个循环中的那些对象。我总是更喜欢方法一,把变量声明在其使用的位置附近。 - Bohn
两个版本中都创建了100000个对象。它们之间唯一的区别是在其中一个版本中,最后创建的对象不能立即进行垃圾回收。 - user207421
我认为与GC清理(当然取决于您的对象)相比,创建它们的成本要高得多,因此总体上GC对性能的影响很小。但是,我会尝试一下...如果根本没有可测量的性能差异,我也不会感到惊讶。 - 463035818_is_not_a_number
不,不是可能的重复,肯定是重复。 - Mike Nakis
@MikeNakis 不,不是明确的重复,甚至不可能是重复。那些问题/答案没有提到或者至少没有清楚地提到对象何时变得符合垃圾回收条件。 - webDeveloper
4个回答

5
如果有10万个项目,那么在方法一中将创建10万个对象,并且每个对象将拥有一个引用(myObject),因此它们不符合GC的条件?
不,从垃圾收集器的角度来看,这两种方法都是相同的,即没有内存泄漏。使用方法二时,只要运行以下语句:
myObject = new MyObject();

之前被引用的MyObject成为孤儿(除非在使用该Object时将其传递给另一个方法,在那里该引用被保存),并且有资格进行垃圾回收。
区别在于,一旦循环结束,您仍然可以通过最初在循环外部创建的myObject引用找到MyObject的最后一个实例。

GC是否知道循环执行期间引用何时超出范围,或者只能在方法结束时知道?

首先,只有一个引用,而不是多个引用。在循环中,对象正在失去引用。其次,垃圾回收不会自发启动。所以忘记循环,它甚至可能在方法退出时都不会发生。
请注意,我说过,孤立的对象将变得有资格进行gc,而不是立即被收集。垃圾回收从不实时发生,而是分阶段进行。在标记阶段,所有不再通过活动线程可达的对象都被标记为删除。然后在扫描阶段,内存被回收,并且像碎片整理硬盘一样进行了额外的压缩。因此,它更像是批处理而不是分步操作。
GC并不关心范围或方法。它只寻找未被引用的对象,并且在感觉到需要时执行此操作。您无法强制它。唯一可以确定的是,如果JVM的内存不足,GC将运行,但您无法确切地确定何时会运行。
但是,所有这些都不意味着GC不能在方法执行期间甚至在循环运行期间启动。例如,如果您有一个每隔10分钟处理10,000个消息然后在其中休眠的消息处理器(即bean在循环中等待,执行10,000次迭代,然后再次等待),则GC肯定会开始回收内存,即使该方法尚未完成。

好的观点,我同意。但是在方法一中,第一个对象何时变得可用?是在循环之后还是在第一次迭代之后? - webDeveloper
在第二次迭代中,一旦执行MyObject myObject = new MyObject();语句,第一次迭代中创建的MyObject对象就会成为垃圾回收的对象。在这个方面,两种方法没有区别。 - Ravi K Thapliyal
垃圾回收器是否知道循环执行期间引用何时超出范围,还是只能在方法结束时才知道?下面的EJP评论说它不知道循环期间的情况?我在谷歌上没有找到有用的信息。谢谢。 - webDeveloper
@webDeveloper 添加了一个更新。 - Ravi K Thapliyal

4
您误解了对象何时才能被垃圾回收 - 当它们不再从活动线程可访问时,它们就变得可以被回收。在这种情况下,这意味着:
  • 当它们的唯一引用超出范围时(方法1)。
  • 当它们的唯一引用被赋予另一个值时(方法2)。
因此,在任何一种方法中,MyObject实例都将在每个循环迭代的末尾成为可回收的。两种方法的理论区别在于,JVM需要在方法1中为每次迭代分配新的对象引用内存,但不需要在方法2中这样做。然而,这假定Java编译器和/或即时编译器不会聪明地优化方法1,使其实际上像方法2一样运行。
无论如何,出于以下原因,我会选择更易读且更少出错的方法1:
  • 单个对象引用分配的性能开销微乎其微。
  • 它可能会被优化掉。

好的回答,直截了当 +1 - webDeveloper

0

我不认为在块内声明变量会对性能产生不利影响。

至少在概念上,JVM在方法开始时分配堆栈帧,并在结束时销毁它。因此,将具有累积大小以容纳所有本地变量。

请参见此处的第2.6节: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html

这与其他语言(如C)一致,在其中调整函数/方法执行时的堆栈帧大小是没有明显回报的开销。

因此,无论您在哪里声明它都不会有任何区别。

事实上,在块中声明变量可能有助于编译器意识到堆栈帧的有效大小可以更小:

void foo() {
   int x=6;
   int y=7;
   int z=8;

  //.....
}

对比

void bar() {
   { 
     int x=6;
     //....
   }
   {
     int y=7;
     //....
   }
   {
     int z=8;
     //....
   }
 }

请注意,bar()显然只需要一个本地变量而不是3个。
虽然让堆栈帧变小可能不会对性能产生任何实际影响!
然而,当引用超出范围时,可能会使其所引用的对象可以进行垃圾回收。否则,您需要将引用设置为null,这是一件麻烦和不必要的事情(以及微小的开销)。
毫无疑问,如果(且仅当)您不需要在循环外部访问它们,则应在循环内部声明变量。
在我看来,被阻止的语句(如上例中的bar)被低估了。
如果该方法分阶段进行,您可以使用块保护后续阶段免受变量污染。
通过合适的(简短)注释,这通常比将其拆分为许多私有方法更易于阅读(并且更有效)。
我有一个庞大的算法(Hashlife),在该方法中使早期构件可供垃圾回收可以使到达结尾与获取OutOfMemoryError之间的区别。

我不同意 - 我认为如果一个方法中有可以自然地分成块的部分,这些块之间共享数据很少,那么这就是将它们重构为单独方法的一个很强的提示。 - BarrySW19
@BarrySW19 我同意。但有时它不是最小共享数据。我的 bar() 是一个玩具示例。假设每个部分都为最终块建立了许多贡献。在Java中,从方法返回超过一个值就会变得很麻烦(和繁琐)。如果您注释掉这些部分并使用“折叠编辑器”,您将开始意识到将代码分解为许多方法远非唯一且始终最佳的结构方式。方法用于重复使用。块用于结构!我知道这是异端邪说。 - Persixty

0
在这两种方法中,对象都将被垃圾回收。
在第一种方法中:当for循环退出时,所有在for循环内部的局部变量都将被垃圾回收,因为循环结束了。
在第二种方法中:当新引用被分配给myObject变量时,早期引用没有适当的引用。因此早期引用会被垃圾回收,直到循环运行结束。
因此,在这两种方法中都没有性能瓶颈。

不是很确定,我认为GC足够聪明,可以收集超出范围的对象。请参考Mike、Ravi和Barry的答案。 - webDeveloper
作为一名Web开发者,垃圾回收器不能确切地知道何时引用超出范围,除非是在方法结束时。没有任何字节码指令对应于内部的“}”,因此不可能有任何相关的JVM、HotSpot或GC操作。 - user207421
@EJP,我说在方法一中,在第一次迭代创建的第一个对象将在第二次迭代中成为GC的对象,如果在第二次迭代期间运行GC,则会将其清除。我认为这是Ravi和BarrySW19所说的吗? - webDeveloper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接