在Java中使用final变量是否可以改善垃圾收集?

时间:2008-11-20 21:08:55

标签: java garbage-collection final

今天,我和我的同事讨论了Java中final关键字的用法,以改进垃圾收集。

例如,如果您编写如下方法:

public Double doCalc(final Double value)
{
   final Double maxWeight = 1000.0;
   final Double totalWeight = maxWeight * value;
   return totalWeight;  
}

声明方法final中的变量将有助于垃圾收集在方法退出后从方法中未使用的变量中清除内存。

这是真的吗?

15 个答案:

答案 0 :(得分:81)

这是一个稍微不同的例子,一个是最终的引用类型字段而不是最终的值类型局部变量:

public class MyClass {

   public final MyOtherObject obj;

}

每次创建MyClass实例时,您都将创建对MyOtherObject实例的传出引用,GC必须按照该链接查找实时对象。

JVM使用标记扫描GC算法,该算法必须检查GC“根”位置中的所有实时引用(如当前调用堆栈中的所有对象)。每个活动对象被“标记”为活着,活动对象引用的任何对象也被标记为活着。

完成标记阶段后,GC扫描堆,释放所有未标记对象的内存(并为剩余的活动对象压缩内存)。

此外,重要的是要认识到Java堆内存被划分为“年轻一代”和“老一代”。所有对象最初都分配在年轻一代(有时称为“托儿所”)。由于大多数对象都是短命的,因此GC更积极地从年轻一代中释放最近的垃圾。如果一个物体在年轻一代的收集周期中存活下来,它将被移动到旧一代(有时被称为“终身代”),这种处理的频率较低。

所以,在我的头脑中,我会说“不,'最终'修饰符无法帮助GC减少其工作量。”

在我看来,优化Java内存管理的最佳策略是尽快消除虚假引用。您可以在完成使用后立即将“null”分配给对象引用。

或者,更好的是,最小化每个声明范围的大小。例如,如果在1000行方法的开头声明一个对象,并且如果该对象保持活动直到该方法的作用域(最后一个结束大括号)结束,那么该对象可能会保持活动的时间长得多必要的。

如果你使用只有十几行代码的小方法,那么在该方法中声明的对象将更快地超出范围,并且GC将能够在很多范围内完成大部分工作。更有效率的年轻一代。除非绝对必要,否则您不希望将对象移动到旧代。

答案 1 :(得分:37)

声明局部变量final不会影响垃圾收集,它只表示您无法修改变量。在修改已标记为totalWeight的变量final时,上面的示例不应编译。另一方面,声明一个原语(double而不是Doublefinal将允许将该变量内联到调用代码中,这样可能会导致一些内存和性能提升。当您在班级中有多个public static final Strings时使用此选项。

通常,编译器和运行时将优化它的位置。最好适当地编写代码,而不是试图太棘手。如果不希望修改变量,请使用final。假设编译器将执行任何简单的优化,如果您担心性能或内存使用,请使用分析器来确定真正的问题。

答案 2 :(得分:25)

不,这显然不是真的。

请记住,final并不意味着不变,只是意味着您无法更改参考。

final MyObject o = new MyObject();
o.setValue("foo"); // Works just fine
o = new MyObject(); // Doesn't work.

基于JVM永远不必修改引用的知识可能会有一些小的优化(例如没有检查它是否已经改变)但是它会很小而不用担心。

Final应该被认为是开发人员有用的元数据,而不是编译器优化。

答案 3 :(得分:15)

要澄清一些要点:

  • 取消引用不应该帮助GC。如果是,则表明您的变量超出范围。对象裙带关系的例外是一个例外。

  • 目前还没有Java中的堆栈分配。

  • 声明变量final表示您不能(在正常条件下)为该变量分配新值。由于final没有说明范围,因此没有说明它对GC的影响。

答案 4 :(得分:11)

好吧,我不知道在这种情况下使用“最终”修饰符,或者它对GC的影响。

但我可以告诉你:你使用Boxed值而不是原语(例如,Double而不是double)会在堆上而不是堆栈上分配那些对象,并会产生不必要的垃圾GC必须清理。

我只在现有API需要时使用盒装基元,或者当我需要可空的灵长类动物时。

答案 5 :(得分:5)

最初赋值后不能更改最终变量(由编译器强制执行)。

这不会改变垃圾收集的行为。唯一的问题是,当不再使用这些变量时,这些变量无法被置换(这可能有助于在内存紧张的情况下进行垃圾收集)。

您应该知道final允许编译器对要优化的内容做出假设。内联代码,不包括已知无法访问的代码。

final boolean debug = false;

......

if (debug) {
  System.out.println("DEBUG INFO!");
}

println不会包含在字节代码中。

答案 6 :(得分:3)

GC对无法访问的引用进行操作。这与“最终”无关,“最终”仅仅是一次性作业的断言。有些VM的GC可以使用“最终”吗?我不知道如何或为什么。

答案 7 :(得分:3)

有一个不太知名的角落案例与世代垃圾收集器。 (有关简要说明,请阅读benjismith的答案,以便深入了解最后的文章。)

世代地方选区的想法是,大多数时候只需要考虑年轻一代。扫描根位置以获取参考,然后扫描年轻代对象。在此频繁扫描期间,不会检查旧一代中的对象。

现在,问题来自于一个对象不允许引用更年轻的对象。当一个长期存在的(旧一代)对象获得对一个新对象的引用时,该引用必须由垃圾收集器显式跟踪(参见IBM hotspot JVM collector上的文章),实际上影响了GC性能。

旧对象无法引用较旧对象的原因是,由于未在次要集合中检查旧对象,如果对象的唯一引用保留在旧对象中,则不会对其进行标记,并且在扫荡阶段会被错误地解除分配。

当然,正如许多人所指出的那样,final关键字并没有真正影响垃圾收集器,但它确实保证了如果这个对象在次要集合中存活并使其变为较旧的对象,则永远不会将该引用更改为更年轻的对象堆。

文章:

IBM关于垃圾收集:history,位于hotspot JVMperformance。这些可能不再完全有效,因为它可以追溯到2003/04年,但它们提供了一些易于阅读的对GC的见解。

Sun Tuning garbage collection

答案 8 :(得分:3)

局部变量和参数的

final对生成的类文件没有区别,因此不会影响运行时性能。如果一个类没有子类,HotSpot会将该类视为最终的(如果加载了打破该假设的类,它可以稍后撤消)。我相信方法上的final与类很相似。静态字段上的final可以允许将变量解释为“编译时常量”,并在此基础上由javac进行优化。字段上的final允许JVM自由地忽略发生在之前的关系。

答案 9 :(得分:2)

似乎有许多答案是游荡的猜想。事实是,在字节码级别没有局部变量的最终修饰符。虚拟机永远不会知道你的局部变量被定义为最终变量。

你的问题的答案是强调的。

答案 10 :(得分:1)

所有方法和变量都可以被子类中的默认覆盖。如果我们想要保留子类来覆盖超类的成员,我们可以使用关键字final将它们声明为final。 例如 -       final int a=10;       final void display(){......} 使方法最终确保无论如何都不会更改超类中定义的功能。同样,最终变量的值永远不会改变。最终变量的行为类似于类变量。

答案 11 :(得分:1)

严格说来 instance 字段,final 可能会稍微提高性能,如果特定的GC想要利用它的话。当并发GC发生时(这意味着您的应用程序仍在运行, GC正在进行中),请参见this for a broader explanation,GC在写入和//时必须采用某些障碍或读取完成。我给您的链接几乎可以解释这一点,但实际上要简短一些:GC进行某些并发工作时,所有对堆的读取和写入(当GC正在进行时)都会被“拦截”,并且稍后申请;以便并发GC阶段可以完成它的工作。

对于final实例字段,由于无法修改(除非进行反射),因此可以省略这些障碍。这不仅仅是纯粹的理论。

Shenandoah GC在实践中让它们 (尽管not for long),您可以这样做,例如:

-XX:+UnlockExperimentalVMOptions  
-XX:+UseShenandoahGC  
-XX:+ShenandoahOptimizeInstanceFinals

GC算法中将进行优化,这将使其速度更快。这是因为将没有障碍拦截final,因为任何人都不会修改它们。甚至没有通过反射或JNI。

答案 12 :(得分:0)

我唯一能想到的是编译器可能会优化掉最终变量并将它们作为常量内联到代码中,因此最终没有分配内存。

答案 13 :(得分:0)

绝对,只要使对象的生命更短,从而产生内存管理的巨大好处,最近我们检查了在一个测试中具有实例变量的导出功能和具有方法级局部变量的另一个测试。在负载测试期间,JVM在第一次测试时抛出了错误的错误并且JVM停止了。但在第二次测试中,由于更好的内存管理,成功地获得了报告。

答案 14 :(得分:0)

我唯一希望将局部变量声明为final的时间是:

  • 使它们成为最终版本,以便它们可以与某个匿名类共享(例如:创建守护程序线程并让它从封闭方法访问某些值)

  • 希望让它们成为最终版本(例如:某些不应该被错误覆盖的值)

  
    
      

他们是否有助于快速垃圾收集?
      如果AFAIK对象没有强引用,那么它就成为GC集合的候选对象,在这种情况下,不能保证它们会立即被垃圾收集。一般来说,强引用在超出范围或者用户明确地将其重新分配给空引用时会死亡,因此,将它们声明为final意味着引用将继续存在直到方法存在(除非其范围明确缩小为特定内部块{})因为您无法重新分配最终变量。所以我认为w.r.t Garbage Collection' final' 可能会引入不必要的延迟。