如何说服JVM内联接口方法?

时间:2011-09-27 08:44:36

标签: java optimization jvm compiler-optimization

我有一个以接口为根的类层次结构,并使用抽象基类实现。它看起来像这样:

interface Shape {
  boolean checkFlag();
}

abstract class AbstractShape implements Shape {
  private boolean flag = false;

  protected AbstractShape() { /* compute flag value */ }

  public final boolean checkFlag() { return flag; }
}

interface HasSides extends Shape { 
  int numberOfSides();
}

interface HasFiniteArea extends Shape { 
  double area();
}

class Square extends AbstractShape implements HasSides, HasFiniteArea {

} 

class Circle extends AbstractShape implements HasFiniteArea { 
}

/** etc **/

当我使用VisualVM对正在运行的代码进行采样时,看起来AbstractShape.checkFlag()从未内联并消耗总程序运行时间的14%,这对于一种简单的方法来说是偶然的,甚至是对于一个经常调用的方法。

我在基类上标记了方法final,并且(当前)实现“Shape”接口的所有类都扩展了AbstractShape。

我是否正确解释了VisualVM样本结果?有没有办法说服JVM内联这个方法,还是我需要撕掉接口并使用抽象基类? (我不愿意,因为层次结构包括像HasFiniteArea和HasSides这样的接口,这意味着层次结构没有完美的树形式)

编辑:要清楚,这是一种在任何 Universe 应该内联的方法。在2分钟执行期间,它被称为超过 4.2亿次,因为它没有内联并且仍然是虚拟调用,所以它占运行时的14%。我问的问题是是什么阻止了JVM内联这个方法以及如何解决它?

4 个答案:

答案 0 :(得分:5)

以下是the Wikipedia

的引用
  

一个常见的误解是声明一个类或方法最终   通过允许编译器直接插入来提高效率   方法在任何地方都可以内联。这不完全正确;该   编译器无法执行此操作,因为已加载类   运行时,可能与那些只是版本的版本不同   编译。此外,运行时环境和JIT编译器具有   有关哪些类已经加载,并且能够确切的信息   更好地决定何时内联,无论是否   方法是最终的。

另见this article

答案 1 :(得分:1)

默认编译器阈值为10000. -XX:CompilerThreshold=这意味着在将方法或循环(对于服务器JVM)编译为本机代码之前必须至少调用10000次。

编译完成后,可以内联,但调用堆栈确实显示了这一点。很聪明,知道内联代码来自另一个方法,你永远不会看到一个截断的调用堆栈。

剖析器尝试示例代码并指定时间。它并不总是做得很好而且你得到的方法显然不是消费者被分配CPU时间的时间。 VisualVM是一个免费的分析器,它是用Java实现的。如果您使用像YourKit这样的分析器,您可以获得更准确的结果,因为它使用本机代码,例如不会造成垃圾。

答案 2 :(得分:0)

从文献中我已经通过声明最终方法来阅读JVM的旧版本,它将确定它将内联转换该方法。 现在,您不必将该方法指定为最终优化代码。

您应该让JVM优化代码,并且只有在您明确不希望覆盖该方法时才使方法成为最终方法。 JVM可能不会使您的方法内联,因为考虑到应用程序的其余代码,它自己的优化速度更快。

答案 3 :(得分:0)

经过大量的实验,在界面上调用时,我无法让Sun JDK 6内联这个方法。

幸运的是,涉及的呼叫站点数量有限,而且正在改变

public void paint(Shape shape) {
  if(shape.checkFlag()) { /* do stuff */ }
} 

public void paint(Shape shape) {
  if(((AbstractShape)shape).checkFlag()) { /* .. */ }
}

足以让JVM内联该方法。与原始运行时间为6分钟相比,计算的运行时间下降了13%。