在java中应该在循环内还是循环外声明变量[重复]

Posted

技术标签:

【中文标题】在java中应该在循环内还是循环外声明变量[重复]【英文标题】:Should variables be declared inside the loop or outside the loop in java [duplicate] 【发布时间】:2015-04-12 02:26:54 【问题描述】:

我知道类似的问题之前已经被问过很多次了,但我仍然不确定对象何时符合 GC 条件以及哪种方法更有效。

方法一:

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

方法二:

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

我理解:“通过最小化局部变量的范围,您可以提高代码的可读性和可维护性,并减少出错的可能性”。 (约书亚·布洛赫)。

但是性能/内存消耗如何?在 Java 中,当没有对对象的引用时,会收集垃圾。如果有例如100000 个项目,然后将创建 100000 个对象。在方法一中,每个对象都有一个对它的引用 (myObject),因此它们不符合 GC 条件?

在方法二中,每次循环迭代时,您都会从先前迭代中创建的对象中删除引用。所以在第一次循环迭代之后,对象肯定会开始变得合格。

还是在性能和​​代码可读性和可维护性之间进行权衡?

我误解了什么?

注意: 假设我关心性能并且循环后不需要 myObject。

提前致谢

【问题讨论】:

性能可能是一个问题,但编写正确的代码并将变量声明放在循环中要好得多。 然后衡量性能。在确定需要它之前,永远不要“优化”任何东西。其他任何事情都是疯狂的。 我认为 GC 足够聪明,可以在需要时在每次循环后清除方法二中的那些对象。我总是更喜欢方法一,声明变量更接近它的使用位置。 两个版本都创建了 100000 个对象。它们之间的唯一区别是最后创建的对象在其中一个中不符合 GC 条件。 我希望创建它们比 GC 清理要昂贵得多(当然取决于您的对象),那么总体而言 GC 对性能的影响很小。但是,我会尝试一下……如果性能没有可衡量的差异,我不会感到惊讶 ^ 不,不可能重复,绝对重复。 【参考方案1】:

如果有例如100000 个项目,然后 100000 个对象将在方法一中创建,每个对象都有一个引用 (myObject),因此它们不符合 GC 条件?

不,从垃圾收集器的角度来看,这两种方法的工作方式相同,即没有内存泄漏。使用方法二,只要以下语句运行

myObject = new MyObject();

之前被引用的MyObject 变成了一个孤儿(除非在使用Object 时您将它传递给另一个保存该引用的方法)并且有资格进行垃圾回收。

不同之处在于,一旦循环用完,您仍然可以通过最初在循环外创建的 myObject 引用访问 MyObject 的最后一个实例。


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

首先只有一个参考,而不是参考。这是在循环中未被引用的对象。其次,垃圾收集不会自发启动。所以忘记循环吧,它甚至可能在方法退出时都不会发生。

请注意,我说过,孤立对象有资格进行 gc,而不是它们会立即被收集。垃圾收集永远不会实时发生,它是分阶段发生的。在 mark 阶段,所有通过 live 线程不能 访问的对象都被标记为删除。然后在 sweep 阶段,内存被回收并额外压缩,就像对硬盘驱动器进行碎片整理一样。因此,它更像是批量操作,而不是零碎操作。

GC 并不关心范围或方法本身。它只查找未引用的对象,并且在想这样做时这样做。你不能强迫它。您唯一可以确定的是,如果 JVM 内存不足,GC 会运行,但您无法准确确定它何时会这样做。

但是,所有这些意味着在方法执行时甚至在循环运行时 GC 都无法启动。例如,如果您有一个 Message Processor,它每 10 分钟左右处理 10,000 条消息,然后在其间休眠,即 bean 在循环中等待,执行 10,000 次迭代,然后再次等待; GC 肯定会开始行动以回收内存,即使该方法尚未运行完成。

【讨论】:

好点并同意,但是在方法一中创建的第一个对象何时符合条件?在循环之后还是第一次迭代之后? 在第二次迭代中,一旦MyObject myObject = new MyObject(); 被执行,在第一次 迭代中创建的MyObject 就可以进行GC。在这方面,这两种方法没有区别。 GC 是否知道在循环执行期间引用何时超出范围,或者它只能在方法结束时知道?下面的 EJP 评论说它在循环期间不知道?我在谷歌上没有找到任何有用的东西。谢谢 @webDeveloper 添加了更新。【参考方案2】:

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

至少在概念上,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)正在使用中。

如果一个方法分阶段进行,您可以使用块来保护后面的阶段免受变量污染。

使用合适的(短)cmets,它通常可以是更易读(和更有效)的结构化代码方式,而不是分解它丢失私有方法。

我有一个粗略的算法 (Hashlife),在该算法中让早期的工件可用于垃圾收集可以在到达终点和获得 OutOfMemoryError 之间产生差异。

【讨论】:

我不同意 - 我认为如果您的方法中有部分可以自然地分成具有最少共享数据的块,那么这是一个非常强烈的暗示,它们应该被重构为单独的方法。 @BarrySW19 我同意。但有时它不是最小的共享数据。我的bar() 是一个玩具示例。假设每个部分都为最终块构建了许多贡献。在 Java 中,从一个方法返回一个以上的值是一个标志(和开销)。如果您评论这些部分并拥有一个“折叠编辑器”,您将开始意识到分解成许多方法远非唯一且始终是构建代码的最佳方法。方法是为了重复使用。块用于结构!我知道这是异端。【参考方案3】:

在这两种方法中,对象都会收集垃圾。

在方法 1 中:当 for 循环退出时,for 循环内的所有局部变量都会在循环结束时收集垃圾。

在方法 2 中:当新的新引用被分配给 myObject 变量时,早期没有正确的引用。所以早期得到垃圾收集等等,直到循环运行。

所以在这两种方法中都没有性能瓶颈。

【讨论】:

并非如此,我认为 GC 足够聪明,可以收集超出范围的对象。请参阅 Mike、Ravi 和 Barry 的答案。 @webDeveloper GC 不知道 references 何时超出范围,除非在方法末尾。没有对应于内部 的字节码指令,ergo 不可能有任何关联的 JVM 或 HotSpot 或 GC 操作。 @EJP 我错了,在方法一中,在迭代中创建的第一个对象将有资格在第二次迭代中进行 GC,如果它要在第二次迭代中运行,它将被 GC 拾取.我相信这就是 Ravi 和 BarrySW19 所说的?【参考方案4】:

您误解了对象何时符合 GC 条件 - 当它们不再可从活动线程访问时,它们会这样做。在这种情况下,这意味着:

当对它们的唯一引用超出范围时(方法 1)。 当对它们的唯一引用被分配另一个值时(方法 2)。

因此,无论使用哪种方法,MyObject 的实例都可以在每次循环迭代结束时进行 GC。两种方法之间的区别(理论上)是,JVM 必须在方法 1 中的每次迭代中为新对象引用分配内存,但在方法 2 中则不需要。但是,这假设 Java 编译器和/或即时编译器将方法 1 优化为实际行为类似于方法 2 并不明智。

无论如何,我会选择更易读且不易出错的方法 1,理由是:

单个对象引用分配的性能开销很小。 无论如何,它可能会被优化掉。

【讨论】:

好答案,直接点+1

以上是关于在java中应该在循环内还是循环外声明变量[重复]的主要内容,如果未能解决你的问题,请参考以下文章

JAVA变量声明在循环体内还是循环体外

变量声明在循环体内还是循环体外的争论

java循环创建对象应该在循环体内还是循环体外

在循环体外面定义的变量和里面定义的变量有啥区别?

变量性能 - java [重复]

在循环之前或循环中声明变量之间的区别?