89

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

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

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

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

这是真的?

4

15 回答 15

90

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

public class MyClass {

   public final MyOtherObject obj;

}

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

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

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

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

所以,在我的脑海中,我会说“不,'final' 修饰符不会帮助 GC 减少它的工作量”。

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

或者,更好的是,最小化每个声明范围的大小。例如,如果您在 1000 行方法的开头声明一个对象,并且如果该对象在该方法的范围关闭(最后一个闭合花括号)之前保持活动状态,那么该对象可能会比实际保持活动更长时间必要的。

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

于 2008-11-20T21:46:09.230 回答
38

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

一般来说,编译器和运行时会尽可能优化。最好适当地编写代码,不要试图太棘手。final当您不想修改变量时使用。假设编译器将执行任何简单的优化,如果您担心性能或内存使用,请使用分析器来确定真正的问题。

于 2008-11-20T21:25:25.497 回答
26

不,这绝对不是真的。

请记住,final这并不意味着恒定,它只是意味着您不能更改参考。

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

基于 JVM 永远不必修改引用(例如无需检查它是否已更改)的知识,可能会有一些小的优化,但它会很小以至于不用担心。

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

于 2008-11-20T21:23:36.267 回答
17

需要澄清的几点:

  • 取消引用不应该帮助 GC。如果是这样,则表明您的变量超出了范围。一个例外是客体裙带关系的情况。

  • 到目前为止,Java 中还没有堆栈分配。

  • 声明一个变量 final 意味着您不能(在正常情况下)为该变量分配一个新值。由于 final 没有说明范围,因此它没有说明它对 GC 的影响。

于 2011-06-19T18:25:19.960 回答
11

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

但是我可以告诉你:你使用盒装值而不是原始值(例如,双精度而不是双精度)将在堆而不是堆栈上分配这些对象,并且会产生 GC 必须清理的不必要的垃圾。

我只在现有 API 需要时使用盒装原语,或者当我需要可为空的原语时。

于 2008-11-20T21:13:29.657 回答
5

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

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

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

final boolean debug = false;

......

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

println 不会包含在字节码中。

于 2009-08-30T08:16:29.960 回答
4

分代垃圾收集器有一个不太为人所知的极端案例。(有关简要说明,请阅读benjismith的答案,以获得更深入的了解,请阅读最后的文章)。

世代 GC 的想法是大多数时候只需要考虑年轻一代。扫描根位置的引用,然后扫描年轻代对象。在这个更频繁的扫描期间,没有检查老一代中的任何对象。

现在,问题来自一个对象不允许引用年轻对象的事实。当长寿命(老一代)对象获得对新对象的引用时,垃圾收集器必须显式跟踪该引用(参见 IBM 关于热点 JVM 收集器的文章),实际上会影响 GC 性能。

旧对象不能引用年轻对象的原因是,由于旧对象没有在次要集合中检查,如果对对象的唯一引用保存在旧对象中,它不会被标记,并且会被错误地标记在扫描阶段释放。

当然,正如许多人所指出的,final 关键字并不会真正影响垃圾收集器,但它确实保证了如果该对象在次要收集中幸存并进入较旧的堆,则该引用永远不会更改为较年轻的对象。

文章:

IBM 谈垃圾收集:历史热点 JVM性能。这些可能不再完全有效,因为它可以追溯到 2003/04,但它们提供了一些易于阅读的 GC 洞察力。

Sun on Tuning 垃圾收集

于 2008-11-20T22:26:37.607 回答
3

GC 作用于无法访问的引用。这与“final”无关,后者只是一次性赋值的断言。是否有可能某些 VM 的 GC 可以使用“final”?我不明白如何或为什么。

于 2008-11-20T21:56:43.563 回答
3

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

于 2008-11-25T18:16:03.383 回答
2

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

你的问题的答案是肯定的。

于 2013-04-17T18:52:38.167 回答
1

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

于 2012-02-07T21:10:59.187 回答
1

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

例如final字段,因为它们不能被修改(除非反射),这些障碍可以被省略。这不仅仅是纯粹的理论。

Shenandoah GC将它们付诸实践(尽管时间不长),您可以这样做,例如:

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

并且GC 算法将进行优化,使其速度稍快一些。这是因为没有障碍拦截final,因为没有人应该修改它们,永远。甚至没有通过反射或 JNI。

于 2019-12-18T12:57:22.890 回答
0

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

于 2008-11-20T21:21:52.433 回答
0

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

于 2012-04-27T18:05:38.290 回答
0

我更喜欢将局部变量声明为 final 的唯一时间是:

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

  • 想让它们成为最终的(例如:一些不应该/不应该被错误覆盖的值)

它们有助于快速垃圾收集吗?
AFAIK 如果一个对象对它的强引用为零,那么它就会成为 GC 收集的候选对象,并且在这种情况下,也不能保证它们会立即被垃圾收集。通常,当强引用超出范围或用户明确将其重新分配给空引用时,就说强引用死亡,因此,将它们声明为最终意味着引用将继续存在直到方法存在(除非其范围明确缩小到一个特定的内部块 {}),因为您不能重新分配最终变量(即不能重新分配给 null)。所以我认为垃圾收集“最终”可能会引入不必要的可能延迟,因此在定义范围时必须小心谨慎,因为它控制了它们何时成为 GC 的候选者。

于 2014-09-16T07:55:38.737 回答