HotSpot是我们众所周知和喜爱的JVM,是Java和Scala汁流淌的大脑。 多年来,许多工程师对其进行了改进和调整,并且在每次迭代中,其代码执行的速度和效率都接近本机编译代码。
JIT(“即时”)编译器是其核心。 该组件的唯一目的是使您的代码快速运行,这是HotSpot如此受欢迎和成功的原因之一。
JIT编译器实际上是做什么的?
在执行代码时,JVM会收集有关其行为的信息。 一旦收集了有关热方法的足够统计信息(默认阈值为10K调用),编译器就会启动,并将该方法的与平台无关的“慢”字节码转换为自身的优化,精简,平均编译版本。
一些优化是显而易见的:简单的方法内联,清除无效代码,用本机数学运算替换库调用等。请注意,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)
也将被删除,因为速度就是它的全部。
生活在边缘
空检查是一丁点的。 有时null对于我们的引用是有效值(例如,指示缺少值或错误),但有时为了安全起见,我们添加了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
的参数l33tify
,则断言将永远不会失败。
在多次执行此代码而没有进入if语句的主体之后,JIT编译器可能会乐观地认为此检查很有可能是不必要的。 然后它将继续编译该方法,将检查全部丢弃,就好像是这样写的:
public static String l33tify(String phrase) {return phrase.replace('e', '3');
}
这可以显着提高性能,这在大多数情况下可能是纯粹的胜利。
但是,如果那个幸福道路的假设最终被证明是错误的呢?
由于JVM现在正在执行本机已编译的代码,因此null引用不会导致模糊的NullPointerException
,而是导致实际的,苛刻的内存访问冲突。 JVM是它的低级生物,它将拦截产生的分段错误,进行恢复,并进行反优化处理-编译器不能再假设null检查是多余的:它重新编译该方法,这次使用null检查。
虚拟精神错乱
JVM的JIT编译器与其他静态编译器(如C ++编译器)之间的主要区别之一是,JIT编译器具有动态运行时数据,决策时可以依靠该数据来运行。
方法内联是一种常见的优化方法,在该方法中,编译器采用一个完整的方法并将其代码插入另一个程序中,以避免调用方法。 在处理虚拟方法调用(或动态调度 )时,这会有些棘手。
以以下代码为例:
public class Main {public static void perform(Song s) {s.sing();}
}public interface Song { void sing(); }public class GangnamStyle implements Song {@Overridepublic void sing() {System.out.println("Oppan gangnam style!");}
}public class Baby implements Song {@Overridepublic void sing() {System.out.println("And I was like baby, baby, baby, oh");}
}// More implementations here
该方法perform
可能被执行数百万次,每一次方法的调用sing
发生。 调用是昂贵的,尤其是诸如此类的调用,因为调用需要根据s
的运行时类型每次动态选择要执行的实际代码。 在这一点上,内联似乎是一个遥不可及的梦想,不是吗?
不必要! 执行后, perform
几千次,编译器可能会决定,根据其收集的统计数据,该调用的95%的目标的一个实例GangnamStyle
。 在这些情况下,HotSpot JIT可以执行乐观优化,以消除虚拟的sing
调用。 换句话说,编译器将为这些代码生成本机代码:
public static void perform(Song s) {if (s fastnativeinstanceof GangnamStyle) {System.out.println("Oppan gangnam style!");} else {s.sing();}
}
由于此优化依赖于运行时信息,因此即使它是多态的,它也可以消除大多数sing
调用。
JIT编译器还有很多技巧,但是这些只是一些技巧,可让您了解当我们的代码由JVM执行和优化时的幕后故事。
我是否能帮助?
JIT编译器是面向简单人员的编译器; 它旨在优化简单的编写,并搜索出现在日常标准代码中的模式。 帮助您的编译器的最好方法是不要太努力地帮助它-只需编写代码即可。
翻译自: https://www.javacodegeeks.com/2013/06/jvm-performance-magic-tricks.html