Java静态编译器(javac)内联一些静态最终变量,并将值直接带到常量池中。考虑以下示例。 A类定义了一些常量(公共静态最终变量):
1 2 3 4
| public class A {
public static final int INT_VALUE = 1000;
public static final String STRING_VALUE ="foo";
} |
B类使用以下常量:
1 2 3 4 5 6 7 8
| public class B {
public static void main (String[] args ) {
int i = A. INT_VALUE;
System. out. println(i );
String s = A. STRING_VALUE;
System. out. println(s );
}
} |
当您编译类B时,javac从类A获取这些常量的值,并在B.class中内联这些值。结果,在编译时必须将类A的依赖项B从字节码中删除。这是一个非常特殊的行为,因为您在编译时正在烘烤这些常量的值。而且您会认为这是JIT编译器在运行时可以做的最简单的事情之一。
有什么方法或任何隐藏的编译器选项可让您禁用Javac的这种内联行为吗?对于背景,我们正在考虑进行字节码分析以实现依赖关系,这是字节码分析无法检测到编译时依赖关系的少数情况之一。谢谢!
编辑:这是一个令人烦恼的问题,因为通常我们不控制所有源代码(例如,定义常量的第三方库)。我们有兴趣从使用常量的角度检测这些依赖关系。由于从使用常量的代码中删除了引用,因此没有简单的方法可以检测到它们,而无需进行源代码分析。
-
好问题!!! 有时,这种内联常数会引起奇怪的问题……但是我还没有看到限制它的正确方法。
Java Puzzlers(Joshua Bloch)的第93项说,您可以通过防止将最终值视为常量来解决此问题。例如:
1 2 3 4
| public class A {
public static final int INT_VALUE = Integer. valueOf(1000). intValue();
public static final String STRING_VALUE ="foo". toString();
} |
当然,如果您无权访问定义常量的代码,则这些都不相关。
-
+1; Bloch确实在Java Puzzlers中对此进行了介绍。他使用ident而不是toX。
-
@DJClayworth:很好。没想到:)对于大多数类型,可能存在现有的方法将起作用(例如String.valueOf())
-
我只记得System.out的分配是这样的:public final static PrintStream out = nullPrintStream();(实际流稍后由本机代码设置)。如果currentTimeMillis()>0,nullPrintStream()返回null。直到现在,我从来没有想到他们为什么要这么做(而不是...out = null;)。当然,如果我不愿意阅读nullInputStream()的文档,那将会很清楚...
-
无法在编译时禁用此优化是javac削弱创建适当调试器功能的多种方式之一。
-
@MarkPeters是其中一项深奥的优化,我喜欢Java核心开发人员。由于类型PrintStream或null引用的对象都不能是编译时常量,因此此内联只能引用JIT,但JIT不在乎该值在赋值之前来自何处。请注意,从Java 7开始,代码的确类似于public final static PrintStream out = null;。
-
对于真正可能是编译时常量的字段,例如在此答案中,使用public static final int INT_VALUE; public static final String STRING_VALUE; static { INT_VALUE = 1000; STRING_VALUE ="foo"; }会更有效,因为避免不必要的方法调用,而不是Integer.valueOf(1000)的装箱开销。
我不相信最简单的解决方法是将它们公开为属性而不是字段:
1 2 3 4 5 6 7 8 9 10 11
| public class A {
private static final int INT_VALUE = 1000;
private static final String STRING_VALUE ="foo";
public static int getIntValue () {
return INT_VALUE ;
}
public static String getStringValue () {
return STRING_VALUE ;
}
} |
不要忘记,在某些情况下,内联对于使用该值必不可少-例如,如果要在开关块中使用INT_VALUE作为案例,则必须将其指定为常量值。
-
乔恩(Jon),您对JLS为什么建议访问器但不私下使用final修饰符(见我的答案)有什么见解?
-
@Mark-如果私有成员为final(结果可以在编译时确定),那么是否有可能内联访问器函数,从而实质上重新创建了public static final方案? (我的推测性推理)
-
@Tim:我的直觉是函数只能在运行时内联。否则,您创建的任何存根方法(return null;)都会要求您在实现该方法后重新编译使用该方法的所有类。
-
@Mark-啊,当然。有点精神上的失败,对此感到抱歉。 :)
-
@Mark-一个原因是,没有代码设置(私有)字段,这使得final模型变得多余……从纯粹的语言意义上来说。 (关于样式的注意事项另有说明,但JLS并未尝试成为样式手册。)
要停止内联,您需要使值成为非编译时间常数(JLS术语)。您可以在不使用函数的情况下执行此操作,而在初始化程序表达式中使用null可以创建最少的字节码。
1
| public static final int INT_VALUE = null!=null?0: 1000; |
尽管javac在其代码生成中非常真实,但应对其进行优化,以使其为紧跟在整数中的立即数,然后在静态初始化程序中将其存储到静态字段。
-
大约1000 + 0。但这是否可以在案件陈述中起作用?
-
@Tuntable 1000+0是一个编译时常量。它与1000没什么不同,因此它可以在case语句中工作,但随后也将内联。答案中的魔力在于null不是编译时常量,因此任何使用它的表达式都不是。
-
如果要确保编译器仅生成一个赋值,则可以将其写为public static final int INT_VALUE; static { INT_VALUE = 1000; }
-
@Holger虽然这是产生完全相同的字节码的更清晰的方法,但我认为还有更多的缺点。 null!=null?0:清楚地表明有做某事的意图,因此不太可能被清洁工拔掉。尽管这个习惯用法并不明显,但即使不是编译时常量语义*,也不需要花费很长时间来弄清楚结果是什么。重新格式化不会完全破坏它。您不必匹配标识符(它们匹配吗?不是吗?这是奇怪的拼写吗?)。
-
我不是坚持要一字不漏;这就是注释的格式。因此,如果格式化程序将其分成多行,那很好。清洁工足够聪明,足以理解null!=null是有意义的,因此在理解编译时常量和初始化程序中分配的变量之间的差异时,应该没有任何问题。由于必须在静态初始化程序中分配未立即分配的static final变量,并且必须只分配一次变量,因此编译器将对其进行检查。这是初始化程序不适合一个表达式的标准习惯用法。
JLS 13.4.9处理此问题。他们的建议是,如果该值可能以任何方式改变,则基本上避免编译时常量。
(One reason for requiring inlining of
constants is that switch statements
require constants on each case, and no
two such constant values may be the
same. The compiler checks for
duplicate constant values in a switch
statement at compile time; the class
file format does not do symbolic
linkage of case values.)
The best way to avoid problems with
"inconstant constants" in
widely-distributed code is to declare
as compile time constants only values
which truly are unlikely ever to
change. Other than for true
mathematical constants, we recommend
that source code make very sparing use
of class variables that are declared
static and final. If the read-only
nature of final is required, a better
choice is to declare a private static
variable and a suitable accessor
method to get its value. Thus we
recommend:
1 2
| private static int N;
public static int getN() { return N; } |
rather than:
1
| public static final int N = ...; |
There is no problem with:
1
| public static int N = ...; |
if N need not be read-only.
像这样重写A类:
1 2 3 4 5 6 7 8 9
| public class A {
public static final int INT_VALUE ;
public static final String STRING_VALUE ;
static {
INT_VALUE = 1000;
STRING_VALUE ="foo";
}
} |
我认为这是一个严重的错误。 Java不是C / C ++。有一个原则(或没有)"编译一次,到处运行"。
在这种情况下,当更改A类时。任何引用A.CONST_VALUE的类都必须重新编译,并且它们几乎不知道是否更改了A类。
-
这不是一个错误,这是一个功能。它可以确保如果在另一个模块中的对象中有一个常量甚至可能没有被加载,那么当您请求BANANA时,它将不会与它一起获得Monkey类和jungle包。 Java提供了反射和序列化(以及其他方法)来避免缓存,但是没有显式强制内联的机制,因此欣快的是它实际上内联了它(如果您删除带有常量的实用程序类,则使用这些常量的类仍然可以工作)。其他解决方案提出了仅通过使用非编译时间常数来实现此目的的方法。
-
这是一个错误。请记住,Java会进行JIT编译,因此这是一种非优化,偶尔会引起严重的麻烦。
-
@Tuntable看起来很简单,当您只考虑读取static字段而不使用值时。但是,想象一下在另一个类的常量上的switch语句应该在编译后应该支持更改时应该如何编译。同样,在代码可到达性方面,该语言也很严格。编译后允许常量更改将破坏语言安全性。并强制所有使用常量使用常量而不是读取动态值是一致的。
-
@Holger javac实际上不是Java编译器。它只是java解析器。是优化的JIT。因此,解释的CASE只是一个哈希表。同样,Java应该像其他所有pre-C语言一样具有关键字可选参数,例如,效率不是C ++的问题。考虑到C#已经有一段时间了,也许我们会看到后者。
-
@Tuntable javac产生字节码。该字节码必须包含转换表的编译形式,而不管其外观如何。现在告诉我们,如果允许在编译后更改键值,则编译后的表单应该是什么样子。您说的是"哈希表",但是哈希码也取决于实际值,因此,不允许更改键值。不要试图用"可选参数"的东西来分散话题的注意力。 C ++或C#是否允许在编译后更改switch语句的键值?我不这么认为。
-
@Holger,就像一个用表达式初始化的数组。或一堆对哈希添加函数的调用。或加载程序可以解析的一些特殊字节代码。现在,我们在哈希表中有字符串,但是仍然需要一些工作。有很多方法。但是,事实并非如此,而且每时每刻都会使人们绊倒。
-
@tuntable switch语句中的字符串仍需要常量标签。这就是开关的工作方式。如果要使用"用表达式初始化的数组"来代替,或者要使用某种可以应付更改键的哈希结构,那么只需实现即可。"很多方法"但不要怪Java没有做与写到源代码中完全不同的事情。
-
@Holger,用于switch语句的常见跳转表优化技术,需要在运行时提供常量。但这在javac时间之后就很好了。可能是加载时间。还是准时。此外,该优化既不必要也不充分。一个好的编译器应该对" if(x == 1)... elseif(x == 2)... elseif(x == 3)..."使用跳转表。同样,没有常量且无法轻松优化的开关也非常有用,并且在其他语言中也受支持。 Java比C复杂得多!
-
@好笑好吧,我应该说"这就是Java开关的工作方式"。 它以这种方式工作,并且需要常量标签。 是的,其他语言支持其他构造。 但这从来不是话题。 主题是Java中的常量字段。 开关在Java中起作用的方式,注释在Java中起作用的方式以及代码可到达性检查在Java中起作用,强制不变,响应的方式。 复制常量值,即使只是隐式。 更改这些内容并不像通过简单的字段读取那样简单。 这才是重点。 强制代码流检查比switch语句更有趣。
jmake是一个开源项目,声称可以完成跟踪Java文件之间的依赖关系并逐步编译所需的最少文件集的全部工作。它声称可以正确处理对静态最终常量的更改,尽管有时需要重新编译整个项目。它甚至比类文件更精细地处理更改。如果(例如)方法C.m()的签名发生更改,则它仅重新编译实际上依赖于m()的类,而不是所有使用C的类。
免责声明:我没有使用jmake的经验。
我最近遇到了一个类似的问题,如上所述,可以使用非编译时表达式来解决这种内联问题,例如:
1 2 3 4 5 6
| public final class A {
public static final int INT_VALUE = constOf (1000);
public static final String STRING_VALUE = constOf ("foo");
} |
constOf方法族仅仅是:
1 2 3 4 5 6 7 8 9 10 11
| // @formatter:off
public static boolean constOf(final boolean value) { return value; }
public static byte constOf(final byte value) { return value; }
public static short constOf(final short value) { return value; }
public static int constOf(final int value) { return value; }
public static long constOf(final long value) { return value; }
public static float constOf(final float value) { return value; }
public static double constOf(final double value) { return value; }
public static char constOf(final char value) { return value; }
public static < T > T constOf(final T value) { return value; }
// @formatter:on |
这比Integer.valueOf(1000).intValue()或null!=null?0: 1000等其他建议短
我觉得Java紧密依赖动态编译,它不像C ++那样执行任何复杂的编译逻辑。
您可以使用JIT编译器尝试一些选项,这些选项可以进行运行时优化,其中某些选项可以禁用/启用此选项。
在默认的javac中,您可能无法获得该选项。你必须使用
1.某种类型的依赖图,例如扩展或实现
2.使用基于方法的链接。
-s
-
我不确定您的意思是什么...这严格来说是javac(静态编译器)的行为,而这似乎是语言规范真正规定的。