Java:JVM如何优化对void和empty函数的调用?

时间:2013-01-08 02:37:36

标签: java jvm javac

让我们假设我们有以下几个类:

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编译器的决定背后的基本原理是什么?

6 个答案:

答案 0 :(得分:16)

  

Java编译器是否会删除语句ab

javac(源到字节码)编译器不会剥离任一调用。 (通过检查字节码可以很容易地检查这一点;例如查看javap -c输出。)

  

在这两种情况下(剥离或不剥离),Java编译器的决定背后的理由是什么?

符合JLS :-)。

从务实的角度来看:

  • 如果javac编译器优化了调用,那么Java调试器根本无法看到它们......这对开发人员来说会相当混乱。
  • 如果javac类和主类独立编译/修改,则早期优化(Message)将导致破坏。例如,请考虑以下序列:

    • Message已编译,
    • 编译主类,
    • 编辑
    • Message以便log执行某些操作并重新编译。

    现在我们有一个编译错误的主类,在ab没有做正确的事情,因为过早的内联代码已经过时了。


但是,JIT编译器可能以各种方式在运行时优化代码。例如:

  • 如果JIT编译器可以推断出不需要虚拟方法调度,则可以内联ab中的方法调用。 (如果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执行的。