在Java中将final用于变量会改善垃圾回收吗?


86

今天,我和我的同事们讨论了如何final在Java中使用关键字来改善垃圾回收。

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

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

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

这是真的?


实际上,这里有件事。1)当您写入方法的本地字段实例时。当您写入实例时,可能会有好处。
尤金(Eugene)

Answers:


86

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

public class MyClass {

   public final MyOtherObject obj;

}

每次创建MyClass实例时,都将创建对MyOtherObject实例的传出引用,GC必须遵循该链接来查找活动对象。

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

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

同样,重要的是要认识到Java堆内存被划分为“年轻”和“旧”。所有对象最初都是在年轻一代中分配的(有时称为“托儿所”)。由于大多数对象都是短命的,因此GC在从年轻一代释放最新垃圾方面更加积极。如果某个对象在年轻一代的收集周期中幸存下来,则将其移入旧一代(有时称为“终身一代”),该对象的处理频率较低。

因此,我想说的是:“不,'最终'修饰语无法帮助GC减少工作量”。

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

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

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


值得深思。我一直认为内联代码会更快,但是如果jvm内存不足,它也会变慢。hmmmmm ...
WolfmanDragon

1
我只是在猜测...但是我假设JIT编译器可以内联最终的基本值(而不是对象),以适度提高性能。另一方面,代码内联能够产生重大的优化,但与最终变量无关。
benjismith

2
它不会是可以分配null以已创建的最终目标,那么也许最终代替帮助,可以使事情更难
埃尔南Eche

1
也可以使用{}来限制大型方法的作用域,而不是将其划分为可能与其他类方法不相关的几个私有方法。
mmm 2012年

您还可以在可能的情况下使用局部变量而不是字段,以增强内存垃圾收集并减少引用关系。
sivi 2014年

37

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

通常,编译器和运行时将在可能的地方进行优化。最好适当地编写代码,而不要太棘手。使用final时,你不希望变量进行修改做。假定编译器将执行任何简单的优化,并且如果您担心性能或内存使用,请使用探查器确定实际问题。


26

不,这很不正确。

请记住,final这并不意味着常数,仅意味着您不能更改参考。

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

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

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


17

需要清除的几点:

  • 淘汰参考文献对GC无效。如果确实如此,则表明您的变量超出了范围。对象裙带关系是一种例外。

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

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


Java中有栈上分配(对原语和对堆中对象的引用)的分配:stackoverflow.com/a/8061692/32453 Java要求与匿名类/ lambda处于同一闭包中的对象也必须是最终对象,但是只是为了减少“所需的内存/帧” /减少混乱,因此与其收集无关...
rogerdpack

11

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

但是我可以告诉你:使用Boxed值而不是基元(例如,使用Double而不是double)将在堆而不是堆栈上分配这些对象,并且将产生GC必须清除的不必要垃圾。

仅在现有API要求或需要可为空的基元时,才使用盒装基元。


1
你是对的。我只需要一个简单的例子来解释我的问题。
Goran Martinic

5

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

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

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

final boolean debug = false;

......

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

println将不包含在字节码中。


@Eugene取决于您的安全管理器,以及编译器是否内联变量。
托尔比约恩Ravn的安徒生

是的,我只是在做书呆子;而已; 也提供了答案
尤金

4

世代垃圾收集器的情况并不那么广为人知。(有关简要说明,请阅读本吉史密斯的答案,以更深入地了解本文末尾的文章)。

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

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

旧对象无法引用较年轻的对象的原因是,由于未在次要集合中检查该旧对象,因此,如果仅将对对象的引用保留在旧对象中,则该对象将不会被标记,并且会错误地进行处理。在清扫阶段被释放。

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

文章:

IBM的垃圾回收:历史热点JVM性能。这些可能不再完全有效,因为它可以追溯到2003/04年,但是它们提供了一些易于理解的GC见解。

太阳在调整垃圾收集


3

GC对无法访问的引用起作用。这与“最终”无关,后者仅是一次分配的主张。某些VM的GC是否可以使用“最终”?我不知道如何或为什么。


3

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


2

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

您的问题的答案很明确。


可能是这样,但是编译器仍可以在数据流分析期间使用最终信息。

@WernerVanBelle编译器已经知道一个变量仅设置一次。它必须已经进行了数据流分析,以便知道变量可能为null,在使用前未初始化等。因此,局部最终不向编译器提供任何新信息。
马特·奎格利

没有。数据流分析可以推断出很多东西,但是有可能在一个块内有一个图灵完整的程序,该程序将设置或不设置局部变量。编译器无法预先知道该变量是否将被写入并将完全为常量。因此,没有final关键字的编译器无法保证变量是否为final。

@WernerVanBelle我真的很感兴趣,您能举个例子吗?我看不到如何对编译器不知道的非最终变量进行最终赋值。编译器知道,如果您有未初始化的变量,它将不允许您使用它。如果您尝试在循环内分配最终变量,编译器将不允许您这样做。在一个可以声明为final的变量但不能声明为final的变量的示例中,编译器不能保证它是final的?我怀疑任何示例都会是一开始就无法声明为final的变量。
马特·奎格利

重新阅读您的注释后,我看到您的示例是在条件块中初始化的变量。除非所有条件路径都一次初始化变量,否则这些变量首先不能是最终变量。因此,编译器确实知道这些声明-这就是它允许您编译在两个不同位置初始化的最终变量的方式(请考虑final int x; if (cond) x=1; else x=2;)。因此,没有final关键字的编译器可以保证变量是否为final。
马特·奎格利

1

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


1

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

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

Shenandoah GC在实践中使用它们(尽管时间不长),您可以这样做,例如:

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

还有将优化的GC算法,这将使它稍微快一些。这是因为不会有障碍被拦截final,因为没有人会修改它们。甚至没有通过反射或JNI。


0

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


0

绝对,只要使对象的寿命变短(这会带来内存管理的巨大好处),最近,我们就检查了在一个测试中具有实例变量而在方法级局部变量中具有实例变量的导出功能。在负载测试期间,JVM在第一次测试时抛出了内存不足错误,并且JVM被暂停。但在第二次测试中,由于更好的内存管理,成功获得了报告。


0

我唯一喜欢将局部变量声明为final的情况是:

  • 必须将它们定型,以便可以与某些匿名类共享(例如:创建守护程序线程,并使其从封闭方法访问某些值)

  • 将它们定为最终值(例如:不应/不会被错误覆盖的某些值)

它们有助于快速垃圾收集吗?
AFAIK如果对象的强引用为零,则该对象将成为GC收集的候选对象,在这种情况下,也无法保证将立即对其进行垃圾收集。通常,当强引用超出范围或用户将其重新分配为空引用时,该强引用就会消失,因此,最终声明它们意味着在方法存在之前引用将继续存在(除非将其范围显式缩小为特定的内部块{}),因为您不能重新分配最终变量(即不能重新分配为null)。因此,我认为垃圾收集“最终”可能会引入不必要的可能的延迟,因此在定义范围时必须小心一点,因为该范围控制它们何时将成为GC的候选对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.