14

假设我们有以下类:

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 编译器会去掉语句ab吗?在这两种情况下(剥离或不剥离),Java 编译器决定背后的基本原理是什么?

4

6 回答 6

17

Java 编译器会去掉语句ab?

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

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

符合 JLS :-)。

从务实的角度来看:

  • 如果javac编译器优化了调用,Java 调试器就根本看不到它们……这对开发人员来说是相当混乱的。
  • 如果类和主类是独立编译/修改的,早期优化(by javac)将导致损坏。Message例如,考虑这个序列:

    • Message被编译,
    • 编译主类,
    • Message被编辑以便log做一些事情......并重新编译。

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


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

  • 如果 JIT 编译器可以推断出不需要虚拟方法分派,则方法调用a并可能被内联。b(如果Logger是应用程序使用的唯一实现ILogger这一点的类,那么对于一个好的 JIT 编译器来说,这是不费吹灰之力的。)

  • 在内联第一个方法调用之后,JIT 编译器可能会确定主体是 noop 并优化调用。

  • 在第二个方法调用的情况下,JIT 编译器可以进一步推断(通过转义分析)Message对象不需要在堆上分配......或者实际上根本不需要。

(如果你想知道 JIT 编译器(在你的平台上)实际上做了什么,Hotspot JVM 有一个 JVM 选项,可以为选定的方法转储 JIT 编译的本机代码。)

于 2013-01-08T03:37:26.320 回答
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 编译器可能会在程序运行时进行进一步优化,这可能能够消除调用。不幸的是,我对细节知之甚少,无法对此发表评论。

旁注:您可能对此链接感兴趣,该链接涉及 Hotspot JVM 使用的性能技术:

https://wikis.oracle.com/display/HotSpotInternals/PerformanceTechniques

于 2013-01-08T02:50:21.487 回答
3

可能最终,绝对不是立即,也不一定永远。JIT 不做任何保证,尤其是对于只被调用几次的方法。(它可能会被归类为简单地内log调用,而内联代码恰好是......什么都没有。)

于 2013-01-08T02:44:48.607 回答
3

不可能明确地说- 这将取决于 JVM / Java 编译器的实现。

一个足够聪明的编译器可以证明这两个语句都没有效果,因此可以消除它们。我相信大多数现代 JVM 都会这样做,尽管您需要测试您的特定配置才能确定。

a) 比 b) 更容易优化,因为 b) 包含一个构造函数调用,编译器还需要证明它没有副作用,然后才能优化整个语句。

请注意,您希望 JIT 编译器而不是 Java 编译器本身完成这种消除,即可能会生成包含日志函数调用的字节码,但稍后 JIT 编译器会在编译时对其进行优化到本机代码。

此外,由于 JIT 可以根据运行时统计信息等重新编译,因此代码可能会在那里开始,但稍后在连续优化时被编译掉。

于 2013-01-08T02:48:25.923 回答
1

如果您将引用设为 final,服务器 JIT 肯定会内联并最终完全消除代码: final ILogger l = new Logger(); 在现代 JVM 中,大部分优化都是由 JIT 执行的。

于 2013-12-06T22:44:37.863 回答
1

我不认为 java 编译器会删除调用,因为被调用的方法是空的,因为您将来也可以更改方法,而无需对 main 方法进行任何更改。

于 2013-01-08T02:45:55.170 回答