让我们假设我们有以下几个类:
public class Message extends Object {}
public class Logger implements ILogger {
public void log(Message m) {/*empty*/}
}
以及以下程序:
public static void main(String args[]) {
ILogger l = new Logger();
l.log((Message)null); // a)
l.log(new Message()); // b)
}
Java编译器是否会删除语句 a 和 b ?在这两种情况下(剥离或不剥离),Java编译器的决定背后的基本原理是什么?
答案 0 :(得分:16)
Java编译器是否会删除语句
a
和b
?
javac
(源到字节码)编译器不会剥离任一调用。 (通过检查字节码可以很容易地检查这一点;例如查看javap -c
输出。)
在这两种情况下(剥离或不剥离),Java编译器的决定背后的理由是什么?
符合JLS :-)。
从务实的角度来看:
javac
编译器优化了调用,那么Java调试器根本无法看到它们......这对开发人员来说会相当混乱。如果javac
类和主类独立编译/修改,则早期优化(Message
)将导致破坏。例如,请考虑以下序列:
Message
已编译,Message
以便log
执行某些操作并重新编译。现在我们有一个编译错误的主类,在a
和b
没有做正确的事情,因为过早的内联代码已经过时了。
但是,JIT编译器可能以各种方式在运行时优化代码。例如:
如果JIT编译器可以推断出不需要虚拟方法调度,则可以内联a
和b
中的方法调用。 (如果Logger
是实现ILogger
的应用程序使用的唯一类,这对于良好的JIT编译器来说是明智的。)
在内联第一个方法调用之后,JIT编译器可能会确定该主体是否为noop并优化了该调用。
在第二个方法调用的情况下,JIT编译器可以进一步推断(通过转义分析)Message
对象不需要在堆上分配...或者确实没有。
(如果你想知道JIT编译器(在你的平台上)实际上做了什么,Hotspot JVM有一个JVM选项,可以为所选方法转储JIT编译的本机代码。)
答案 1 :(得分:6)
反汇编以下文件(使用javap -c
)表明在编译为字节码时,1.7.0编译器不会将它们删除:
public class Program
{
public static class Message extends Object {}
public interface ILogger {
void log(Message m);
}
public static class Logger implements ILogger {
public void log(Message m) { /* empty */ }
}
public static void main(String[] args) {
ILogger l = new Logger();
l.log((Message)null); // a)
l.log(new Message()); // b)
}
}
结果如下。关键位是第13和26行的调用。
Compiled from "Program.java"
public class Program {
public Program();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class Program$Logger
3: dup
4: invokespecial #3 // Method Program$Logger."<init>":()V
7: astore_1
8: aload_1
9: aconst_null
10: checkcast #4 // class Program$Message
13: invokeinterface #5, 2 // InterfaceMethod Program$ILogger.log:(LProgram$Message;)V
18: aload_1
19: new #4 // class Program$Message
22: dup
23: invokespecial #6 // Method Program$Message."<init>":()V
26: invokeinterface #5, 2 // InterfaceMethod Program$ILogger.log:(LProgram$Message;)V
31: return
}
编辑:但是,正如@mikera指出的那样,JIT编译器可能会在程序运行时进行进一步的优化,这可能会消除调用。不幸的是,我对细节的评论不够充分。
SIDE注意:您可能对此链接感兴趣,该链接涉及Hotspot JVM使用的性能技术:
https://wikis.oracle.com/display/HotSpotInternals/PerformanceTechniques
答案 2 :(得分:3)
可能最终,绝对不是立即,也不一定是永远。 JIT不保证,特别是对于只被调用几次的方法。 (它可能被归类为简单内联 log
调用,并且内联代码恰好是......什么都没有。)
答案 3 :(得分:3)
无法确切地说 - 它将取决于JVM / Java编译器的实现。
足够聪明的编译器可以证明这两个语句都没有效果,因此可以消除它们。我相信大多数现代JVM都会这样做,但你需要测试你的特定配置才能确定。
a)比b)更容易优化,因为b)包含一个构造函数调用,编译器在证明整个语句优化之前还需要证明它没有副作用。请注意,您希望这种消除是由JIT编译器而不是Java编译器本身完成的,即可能会生成包含日志函数调用的字节码,但稍后由JIT编译器对其进行优化当它编译为本机代码时。
此外,由于JIT可以重新编译运行时统计信息等,因此代码可能会从那里开始,但在后续的优化中会被编译掉。
答案 4 :(得分:1)
我认为java编译器不会删除调用,因为被调用的方法是空的,因为您可以在以后更改方法而不对main方法进行任何更改。
答案 5 :(得分:1)
如果您将参考文章设为最终版本,服务器JIT肯定会内联并最终消除代码: 最终的ILogger l = new Logger(); 在现代JVM中,大多数优化都是由JIT执行的。