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

Eri*_*son 6 java debugging performance final compiler-optimization

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

if (GVar.runInDebugMode) System.out.println("Print debug message");
Run Code Online (Sandbox Code Playgroud)

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

如果我设置:

public final static boolean runInDebugMode = false;
Run Code Online (Sandbox Code Playgroud)

runInDebugMode编译器是否会在每次遇到如下语句时重新计算:if (GVar.runInDebugMode)或者由于它被声明为final,因此它将在程序开始时计算一次,并且不会对CPU造成额外的压力?换句话说,一旦部署应用程序或设置足够,我最好完全注释掉所有调试语句runInDebugModefalse

Hol*_*ger 3

当你声明一个变量时,比如

\n
public final static boolean runInDebugMode = false;\n
Run Code Online (Sandbox Code Playgroud)\n

it\xe2\x80\x99是一个编译时常量

\n
\n

常量变量final原始类型或使用常量表达式 ( \xc2\xa715.29String )初始化的类型的变量。

\n
\n

意思就是

\n
\n

对常量变量 ( \xc2\xa74.12.4 )字段的引用必须在编译时解析为V常量变量的初始值设定项表示的值。

\n

如果这样的字段是static,则二进制文件的代码中不应存在对该字段的引用,包括声明该字段的类或接口。

\n
\n

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

\n

您的用例已在\xc2\xa714.22中专门讨论

\n
\n

然而,为了让该if语句能够方便地用于“条件编译”目的,实际规则有所不同。

\n

例如,以下语句会导致编译时错误:

\n
while (false) { x=3; }\n
Run Code Online (Sandbox Code Playgroud)\n

因为该语句x=3;不可到达;但表面上相似的情况:

\n
if (false) { x=3; }\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

这种不同处理的基本原理是允许程序员定义“标志”变量,例如:

\n
static final boolean DEBUG = false;\n
Run Code Online (Sandbox Code Playgroud)\n

然后编写代码,例如:

\n
if (DEBUG) { x=3; }\n
Run Code Online (Sandbox Code Playgroud)\n

这个想法是应该可以更改DEBUGfrom falsetotrue或 from trueto的值false,然后正确编译代码,而不需要对程序文本进行其他更改。

\n

条件编译附带一个警告。如果编译了一组使用“标志”变量(或更准确地说,任何static常量变量(\xc2\xa74.12.4))的类,并且省略了条件代码,则稍后仅分发该类的新版本是不够的。包含标志定义的类或接口。

\n
\n

因此,该声明清楚地表明,这种形式的条件编译符合语言设计者的意图,并且编译器有权省略有问题的代码(所有相关编译器都这样做)。原则上,编译器不需要省略代码,但由于它不能在编译后的代码中生成对该字段的引用GVar.runInDebugMode,因此代码可以\xe2\x80\x99t包含真正的条件。如果代码没有被省略,那么它必须以事实上无条件的方式被跳过。要么通过goto指令,要么以可以想象到的最 na\xc3\xafve 方式编译时,通过逐字测试false, iconst_0; ifeq \xe2\x80\xa6。这两种方法在解释执行模式下都是纳秒级的,并且对 JIT 编译器/优化器没有任何挑战。

\n
\n

值得一提的是,static final字段是受信任的字段,通常甚至不能通过反射进行更改。这是由断言功能使用的,因为在幕后,包含assert语句的类将有一个static final boolean在类初始化时初始化的字段(因此 it\xe2\x80\x99s 不是编译时常量),并且每个assert语句将跳过根据static final变量的状态进行有条件的检查。早在Java\xc2\xa01.4的时候,就断定必要的死代码消除在JVM中是司空见惯的,所以就以这种方式依赖它。

\n

因此,即使将调试标志从编译时常量转换为初始化时常量,对性能的影响也几乎不会被注意到。但按照您现在使用它的方式,代码已在编译时删除,并且无论如何都不依赖 JVM。

\n