JVM的几点性能优化

发表于:2014-03-31来源:不祥作者:Java译站点击数: 标签:性能优化
JVM的几点性能优化 otSpot,家喻户晓的JVM,我们的Java和Scala程序就运行在它上面。年复一年,一次又一次的迭代,经过无数工程师的不断优化,现在它的代码执行的速度和效率已经逼近本地编译的代码了。

  otSpot,家喻户晓的JVM,我们的Java和Scala程序就运行在它上面。年复一年,一次又一次的迭代,经过无数工程师的不断优化,现在它的代码执行的速度和效率已经逼近本地编译的代码了。

  它的核心是一个JIT(Just-In-Time)编译器。JIT只有一个目的,就是为了提升你代码的执行速度,这也是HotSpot能如此流行和成功的重要因素。

  JIT编译器都做了什么?

  你的代码在执行的时候,JVM会收集它运行的相关数据。一旦收集到了足够的数据,证明某个方法是热点(默认是1万次调用),JIT就会介入进来,将“运行缓慢的”平台独立的的字节码转化成本地编译的,优化瘦身后的版本。

  有些优化是显而易见的:比如简单方法内联,删除无用代码,将库函数调用替换成本地方法等。不过JIT编译的威力远不止此。下面列举了它的一些非常有意思的优化:

  分而治之

  你是不是经常会这样写代码:

  StringBuilder sb = new StringBuilder("Ingredients: ");

  for (int i = 0; i < ingredients.length; i++) {

  if (i > 0) {

  sb.append(", ");

  }

  sb.append(ingredients[i]);

  }

  return sb.toString();

  或者这样:

  boolean nemoFound = false;

  for (int i = 0; i < fish.length; i++) {

  String curFish = fish[i];

  if (!nemoFound) {

  if (curFish.equals("Nemo")) {

  System.out.println("Nemo! There you are!");

  nemoFound = true;

  continue;

  }

  }

  if (nemoFound) {

  System.out.println("We already found Nemo!");

  } else {

  System.out.println("We still haven't found Nemo : (");

  }

  }

  这两个例子的共同之处是,循环体里先是处理这个事情,过一段时间又处理另外一件。编译器可以识别出这些情况,它可以将循环拆分成不同的分支,或者将几次迭代单独剥离。

  我们来说下第一个例子。if(i>0)第一次的时候是false,后面就一直是true。为什么要每次都判断这个呢?编译器会对它进行优化,就好像你是这样写的一样:

  StringBuilder sb = new StringBuilder("Ingredients: ");

  if (ingredients.length > 0) {

  sb.append(ingredients[0]);

  for (int i = 1; i < ingredients.length; i++) {

  sb.append(", ");

  sb.append(ingredients[i]);

  }

  }

  return sb.toString();

  这样写的话,多余的if(i > 0)被去掉了,尽管也带来了一些代码重复(两处append),不过性能上得到了提升。

  边界条件优化

  检查空指针是很常见的一个操作。有时候null是一个有效的值(比如,表明缺少某个值,或者出现错误),有时候检查空指针是为了代码能正常运行。

  有些检查是永远不会失败的(在这里null代表失败)。这里有一个典型的场景:

  public static String l33tify(String phrase) {

  if (phrase == null) {

  throw new IllegalArgumentException("phrase must not be null");

  }

  return phrase.replace('e', '3');

  }

  如果你代码写得好的话,没有传null值给l33tify方法,这个判断永远不会失败。

  在多次执行这段代码并且一直没有进入到if语句之后,JIT编译器会认为这个检查很多可能是多余的。然后它会重新编译这个方法,把这个检查去掉,最后代码看起来就像是这样的:

  public static String l33tify(String phrase) {

  return phrase.replace('e', '3');

  }

  这能显著的提升性能,而且在很多时候这么优化是没有问题的。

  那万一这个乐观的假设实际上是错了呢?

  JVM现在执行的已经是本地代码了,空引用可不会引起NullPointerException,而是真正的严重的内存访问冲突,JVM是个低级生物,它会去处理这个段错误,然后恢复执行没有优化过的代码——这个编译器可再也不敢认为它是多余的了:它会重新编译代码,这下空指针的检查又回来了。

  虚方法内联

  JVM的JIT编译器和其它静态编译器的最大不同就是,JIT编译器有运行时的动态数据,它可以基于这些数据进行决策。

  方法内联是编译器一个常见的优化,编译器将方法调用替换成实际调用的代码,以避免一次调用的开销。不过当碰到虚方法调用(动态分发)的话情况就需要点小技巧了。

  先看下这段代码 :

  public class Main {

  public static void perform(Song s) {

  s.sing();

  }

  }

  public interface Song { void sing(); }

  public class GangnamStyle implements Song {

  @Override

  public void sing() {

  System.out.println("Oppan gangnam style!");

  }

  }

  public class Baby implements Song {

  @Override

  public void sing() {

  System.out.println("And I was like baby, baby, baby, oh");

  }

  }

  perform方法可能会被调用了无数次,每次都会调用sing方法。方法调用的开销当然是很大的,尤其像这种,因为它需要根据运行时s的类型来动态选择具体执行的代码。在这里,方法内联看真来像是遥不可及的梦想,对吧?

原文转自:http://it.deepinmind.com/jvm/2014/03/28/jvm-performance-magic-tricks.html