Java 是否仅对声明为 Final 的变量求值一次?

Does Java Evaluate a Variable Declared as Final only Once?

我正在编写一个 Java 程序,该程序需要数千条 System.out.println() 语句,这些语句将在程序的整个生命周期中打印数亿(或数十亿)次以进行调试:

if (GVar.runInDebugMode) System.out.println("Print debug message");

在现实世界中,可以停用这些语句以加速计算量大的计算。

如果我设置:

public final static boolean runInDebugMode = false;

编译器是否在每次遇到像if (GVar.runInDebugMode)这样的语句时重新计算runInDebugMode,或者因为它被声明为final,所以它会在程序开始时被计算一次并获胜'对 CPU 施加额外的压力?换句话说,我最好在部署应用程序后完全注释掉所有调试语句,或者将 runInDebugMode 设置为 false 就足够了吗?

你的场景描述为“条件编译”,即生成字节码的编译器can optimize away the code if the constant variable is false:

”...为了让if语句方便的用于“条件编译”的目的,实际规则不同

例如,以下语句会导致 compile-time 错误:

while (false) { x=3; }

因为语句x=3;无法到达;但表面上类似的情况:

if (false) { x=3; }

不会导致 compile-time 错误。 优化编译器可能会意识到语句 x=3; 永远不会被执行,并且可能会选择从生成的 class 文件 中省略该语句的代码,但是语句x=3; 在此处指定的技术意义上不被视为“无法访问”。

请注意粗体部分:它取决于编译器,但使用 javap 反汇编程序很容易验证,只需 运行 javap -v -l -p MyClass.class 查看生成的字节码。我很确定 Oracle/OpenJDK javac 很久以前就进行了优化,大多数编译器也是如此。请注意,GVar.runInDebugModeconstant expression,因此它受益于此优化。

切记:

“条件编译附带一个警告。如果一组 classes 使用“标志”变量 - 或者更准确地说,任何静态常量变量(§4.12.4) - 已编译且省略了条件代码,以后仅分发包含标志定义的 class 或接口的新版本是不够的。使用标志的 classes 不会看到它的新值,所以他们的行为可能会令人惊讶。本质上,对标志值的更改与 pre-existing 二进制文件二进制兼容(不会发生 LinkageError),但在行为上不兼容。"

这基本上是说,除了定义它的 class 之外,您还必须 re-compile 使用该标志的代码。如果您构建整个代码,这应该不是问题。

当你声明一个像

这样的变量时
public final static boolean runInDebugMode = false;

这是一个compile-time constant

A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.29).

which means that

A reference to a field that is a constant variable (§4.12.4) must be resolved at compile time to the value V denoted by the constant variable's initializer.

If such a field is static, then no reference to the field should be present in the code in a binary file, including the class or interface which declared the field.

换句话说,当你在任何地方写 if(runInDebugMode) 并且 runInDebugMode 在编译时是 false 时,行为就好像你写了 if(false),因为该值必须在编译时解析,编译后的 class 文件中不会出现对该字段的引用。

您的用例已在 §14.22

中具体讨论

However, in order to allow the if statement to be used conveniently for "conditional compilation" purposes, the actual rules differ.

As an example, the following statement results in a compile-time error:

while (false) { x=3; }

because the statement x=3; is not reachable; but the superficially similar case:

if (false) { x=3; }

does not result in a compile-time error. An optimizing compiler may realize that the statement x=3; will never be executed and may choose to omit the code for that statement from the generated class file, but the statement x=3; is not regarded as "unreachable" in the technical sense specified here.

The rationale for this differing treatment is to allow programmers to define "flag" variables such as:

static final boolean DEBUG = false;

and then write code such as:

if (DEBUG) { x=3; }

The idea is that it should be possible to change the value of DEBUG from false to true or from true to false and then compile the code correctly with no other changes to the program text.

Conditional compilation comes with a caveat. If a set of classes that use a "flag" variable - or more precisely, any static constant variable (§4.12.4) - are compiled and conditional code is omitted, it does not suffice later to distribute just a new version of the class or interface that contains the definition of the flag.

因此,该声明清楚地表明这种形式的条件编译符合语言设计者的意图,并且编译器有权省略有问题的代码(所有相关编译器都这样做)。原则上,编译器不需要省略代码,但由于它 不能 在编译代码中生成对字段 GVar.runInDebugMode 的引用,因此代码不能包含一个真正的条件。如果代码没有省略,则必须de-facto无条件跳过。或者,通过 goto 指令,或者在以可以想象的最天真的方式编译时,通过逐字测试 falseiconst_0; ifeq …。这两种方法在解释执行模式下都是纳秒级的,对 JIT 编译器/优化器根本没有挑战。


值得一提的是,static final 字段是受信任的字段,通常甚至无法通过反射进行更改。这被使用,例如通过断言功能,在幕后,包含 assert 语句的 class 将有一个 static final boolean 字段在 class 初始化时间初始化(因此它不是 compile-time 常量)并且每个 assert 语句将有条件地跳过其检查,具体取决于 static final 变量的状态。早在 Java 1.4 的时候,就得出了必要的死代码消除在 JVM 中司空见惯的结论,以这种方式依赖它。

因此,即使您将调试标志从 compile-time 常量更改为 initialization-time 常量,对性能的影响也很难察觉。但是您现在使用它的方式,代码已在 compile-time 处删除,并且无论如何都不依赖于 JVM。