在 java 中变量应该在循环内还是循环外声明

Should variables be declared inside the loop or outside the loop in java

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

方法一:

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

方法二:

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

我明白了:"By minimizing the scope of local variables, you increase the readability and maintainability of your code and reduce the likelihood of error"。 (约书亚·布洛赫)。

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

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

还是性能与代码可读性和可维护性之间的权衡?

我误会了什么?

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

提前致谢

If there are e.g. 100000 items then 100000 objects will be created in Approach One and each object will have a reference (myObject) to it so they are not eligible for GC?

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

myObject = new MyObject();

被引用的前一个 MyObject 成为孤儿(除非在使用那个 Object 时你将它传递给保存该引用的另一个方法)并且有资格成为垃圾collection.

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


Does GC know when references go out of scope during the loop execution or it can only know at the end of method?

首先只有一个引用,没有引用。在循环中未被引用的是 objects。其次,垃圾 collection 不会自发地进入。所以忘记循环吧,它甚至可能在方法退出时不会发生。

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

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

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

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

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

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

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

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

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

至少理论上 JVM 在方法开始时分配堆栈帧并在结束时销毁它。暗示将具有容纳所有局部变量的累积大小。

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

这与 C 等其他语言一致,其中在 function/method 执行时调整堆栈帧的大小是一种开销,没有明显的 return。

因此,无论您在哪里声明,都不会有什么不同。

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

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 之间的区别。

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

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

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

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