大家好!
今天,请您注意本文的翻译,其中示例编译了在JVM中进行编译的选项。 特别要注意Java 9及更高版本中支持的AOT编译。
祝您阅读愉快!
我想任何曾经用Java编程的人都听说过即时编译(JIT),甚至有可能在执行之前进行编译(AOT)。 此外,无需解释什么是“解释”语言。 本文将解释如何在Java虚拟机JVM中实现所有这些功能。
您可能知道,使用Java进行编程时,需要运行一个编译器(使用“ javac”程序),该编译器将Java源代码(.java文件)收集到Java字节码(.class文件)中。 Java字节码是一种中间语言。 之所以称它为“中间”,是因为它不为真正的计算设备(CPU)所理解,并且不能由计算机执行,因此代表了源代码与处理器中执行的“本机”机器代码之间的过渡形式。
为了使Java字节码能够执行任何特定的工作,有3种方法可以使它执行:
- 直接执行中间代码。 说它需要“解释”是更好和更正确的。 JVM具有Java解释器。 如您所知,要使JVM工作,您需要运行“ java”程序。
- 在执行中间代码之前,将其编译为本机代码,并强制CPU执行此新烘焙的本机代码。 因此,编译恰好在执行之前(及时)进行,因此称为“动态”。
- 3首先,即使在程序启动之前,中间代码也将转换为本地代码,并从头到尾通过CPU运行。 该编译在执行之前完成,称为AoT(提前)。
因此,(1)是解释器的工作,(2)是JIT编译的结果,而(3)是AOT编译的结果。
为了完整起见,我将提到有第四种方法-直接解释源代码,但是在Java中这是不接受的。 例如,这是在Python中完成的。
现在,让我们看看“ java”如何作为(1)(2)JIT编译器和/或(3)AOT编译器的解释器以及何时起作用的。
简而言之-通常,“ java”同时执行(1)和(2)。 从Java 9开始,第三个选项也是可能的。
这是我们的
Test
类,将在以后的示例中使用。
public class Test { public int f() throws Exception { int a = 5; return a; } public static void main(String[] args) throws Exception { for (int i = 1; i <= 10; i++) { System.out.println("call " + Integer.valueOf(i)); long a = System.nanoTime(); new Test().f(); long b = System.nanoTime(); System.out.println("elapsed= " + (ba)); } } }
如您所见,有一个
main
方法可实例化
Test
对象并连续10次循环调用
f
函数。
f
函数几乎不执行任何操作。
因此,如果您编译并运行上述代码,则输出将是预期的(当然,经过时间的值对您而言将有所不同):
call 1 elapsed= 5373 call 2 elapsed= 913 call 3 elapsed= 654 call 4 elapsed= 623 call 5 elapsed= 680 call 6 elapsed= 710 call 7 elapsed= 728 call 8 elapsed= 699 call 9 elapsed= 853 call 10 elapsed= 645
现在的问题是:这个结论是“ java”作为解释器(即选项(1),“ java”作为JIT编译器,即选项(2))的结果还是与AOT编译有关? ,即选项(3)? 在本文中,我将为所有这些问题找到正确的答案。
我想给出的第一个答案很可能只有(1)发生在这里。 我说“最有可能”是因为我不知道这里是否设置了会更改默认JVM选项的环境变量。 如果没有安装多余的东西,并且默认情况下这就是“ java”的工作方式,那么这里我们100%只观察选项(1),也就是说,代码被完全解释了。 我确信这一点,因为:
- 根据Java文档,
-XX:CompileThreshold=invocations
选项是在客户端JVM上以默认invocations=1500
启动的(下面将介绍有关客户端JVM的更多信息)。 由于我只运行10次且10 <1500,所以这里我们不讨论动态编译。 通常,此命令行选项指定在动态编译步骤开始之前,函数必须被解释的次数(最大)。 我将在下面对此进行详细介绍。 - 实际上,我运行了带有诊断标志的代码,因此我知道它是否是动态编译的。 我还将在下面解释这一点。
请注意:JVM可以在客户端或服务器模式下工作,并且在第一种情况和第二种情况下默认设置的选项将不同。 通常,将根据环境或启动JVM的计算机自动做出有关启动模式的决定。 在下文中,我将在所有启动过程中指定
–client
选项,以免怀疑程序是否在客户端模式下运行。 此选项不会影响我在本文中要演示的方面。
如果使用
-XX:PrintCompilation
运行“ java”,则在动态编译函数时,程序将打印一行。 不要忘记对每个函数分别执行JIT编译,该类中的某些函数可能仍保留在字节码中(即未编译),而其他一些函数可能已经通过了JIT编译,即准备在处理器中直接执行。
在下面,我还添加了
-Xbatch
选项。 仅需要
-Xbatch
选项才能使输出看起来更可呈现。 否则,JIT编译会竞争性地进行(连同解释),并且编译后的输出有时在运行时可能看起来很奇怪(由于
-XX:PrintCompilation
)。 但是,
–Xbatch
选项禁用后台编译,因此,在执行JIT编译之前,将停止执行程序。
(为了便于阅读,我将在新行中写入每个选项)
$ java -client -Xbatch -XX:+PrintCompilation Test
我不会在此处插入此命令的输出,因为默认情况下,JVM会编译许多内部函数(例如,与java,sun,jdk包相关),因此输出将非常长-因此,在我的屏幕上,内部函数上有274行以及更多内容-该程序的最终结论)。 为了
Test.f
这项研究,我将取消内部类的JIT编译,或者仅对我的方法(
Test.f
)有选择地启用它。 为此,再指定一个选项
-XX:CompileCommand
。 您可以指定许多命令(编译),因此将它们放在单独的文件中会更容易。 幸运的是,我们有
-XX:CompileCommandFile
选项
-XX:CompileCommandFile
。 因此,继续创建文件。 我称其为
hotspot_compiler
,其原因是我稍后将进行解释并编写以下内容:
quiet exclude java/* * exclude jdk/* * exclude sun/* *
在这种情况下,应该完全清楚我们从以java,jdk和sun开头的所有程序包中排除所有类中的所有函数(最后一个*)(程序包名称以/分隔,您可以使用*)。
quiet
命令告诉JVM不要写任何有关被排除的类的信息,因此只有现在编译的那些类才会输出到控制台。 因此,我运行:
java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler Test
在告诉您该命令的输出之前,我提醒您将文件命名为
hotspot_compiler
,因为在Oracle JDK中似乎(我没有检查)使用编译器命令为文件默认设置了
.hotspot_compiler
名称。
因此,结论是:
many lines like this 111 1 n 0 java.lang.invoke.MethodHandle::linkToStatic(LLLLLL)L (native) (static) call 1 some more lines like this 161 48 n 0 java.lang.invoke.MethodHandle::linkToStatic(ILIJL)I (native) (static) elapsed= 7558 call 2 elapsed= 1532 call 3 elapsed= 920 call 4 elapsed= 732 call 5 elapsed= 774 call 6 elapsed= 815 call 7 elapsed= 767 call 8 elapsed= 765 call 9 elapsed= 757 call 10 elapsed= 868
首先,我不知道为什么某些
java.lang.invoke.MethodHandler.
方法仍在编译
java.lang.invoke.MethodHandler.
可能有些事情无法关闭。 据我了解,这是最新消息。 但是,您可以看到,所有其他编译步骤(以前只有274行)现在都消失了。 在其他示例中,我还将从编译日志的输出中删除
java.lang.invoke.MethodHandler
。
让我们看看我们得出的结论。 现在,我们有一个简单的代码,可以在其中运行函数10次。 如前所述,我曾提到此功能是解释性的,而不是编译性的,如文档中所示,现在我们在日志中看到了它(同时,我们在编译日志中没有看到它,这意味着它不受JIT编译的影响)。 好吧,您刚刚看到了“ java”工具的作用,仅在100%的情况下解释并解释了我们的功能。 因此,我们可以选中带有选项(1)的复选框。 我们传递给(2),动态编译。
根据文档,您可以运行该函数1500次,并确保JIT编译确实正在进行。 但是,您也可以使用
-XX:CompileThreshold=invocations
调用
-XX:CompileThreshold=invocations
,设置所需的值而不是1500。 让我们在这里指出5。这意味着我们期望:在对函数f进行5次“解释”之后,JVM必须编译该方法,然后运行已编译的版本。
java-客户端-Xbatch
-XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 Test
如果运行此命令,您可能会注意到与上面的示例相比,没有任何变化。 即,仍然不会发生编译。 事实证明,根据文档,
-XX:CompileThreshold
仅在禁用
TieredCompilation
时才起作用,这是默认设置。 它像这样
-XX:-TieredCompilation
:
-XX:-TieredCompilation
。 分层编译是Java 7中引入的一项功能,可以提高JVM的启动和巡航速度。 在本文的上下文中,它并不重要,请随时禁用它。 现在让我们再次运行此命令:
java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation Test
这是输出(我记得,我已经错过了有关
java.lang.invoke.MethodHandle
的行):
call 1 elapsed= 9411 call 2 elapsed= 1291 call 3 elapsed= 862 call 4 elapsed= 1023 call 5 227 56 b Test::<init> (5 bytes) 228 57 b Test::f (4 bytes) elapsed= 1051739 call 6 elapsed= 18516 call 7 elapsed= 940 call 8 elapsed= 769 call 9 elapsed= 855 call 10 elapsed= 838
我们欢迎(您好!)在调用数字5之后立即动态编译函数Test.f或
Test::<init>
,因为我将CompileThreshold设置为5。JVM对该函数进行了5次解释,然后对其进行编译,最后运行编译版本。 由于该函数已编译,因此它应该运行得更快,但由于此函数不执行任何操作,因此我们无法在此进行验证。 我认为这是单独发帖的好话题。
您可能已经猜到了,这里编译了另一个函数
Test::<init>
,它是
Test
类的构造函数。 由于代码调用了构造函数(new
Test()
),因此无论何时调用
f
,它都会在调用5次之后与
f
函数同时编译。
原则上,这可以结束对选项(2)(JIT编译)的讨论。 如您所见,在这种情况下,该函数首先由JVM解释,然后经过五重解释后动态编译。 我想添加有关JIT编译的最后一个细节,即提及选项
-XX:+PrintAssembly
。 顾名思义,它会将功能的编译版本输出到控制台(编译版本=本机代码=汇编代码)。 但是,这仅在库路径中存在反汇编程序时才有效。 我猜反汇编程序在不同的JVM中可能有所不同,但是在这种情况下,我们要处理的是hsdis-openjdk的反汇编程序。 hsdis库的源代码或其二进制文件可以放在不同的位置。 在这种情况下,我编译了此文件,并将
hsdis-amd64.so
放入
JAVA_HOME/lib/server
。
现在,我们可以执行此命令。 但是首先我必须添加它以运行
-XX:+PrintAssembly
还需要添加
-XX:+UnlockDiagnosticVMOptions
,并且它必须在
PrintAssembly
选项之前。 如果不这样做,那么JVM将向您发出有关
PrintAssembly
选项使用不正确的警告。 让我们运行以下代码:
java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test
输出将很长,并且将显示以下行:
0x00007f4b7cab1120: mov 0x8(%rsi),%r10d 0x00007f4b7cab1124: shl $0x3,%r10 0x00007f4b7cab1128: cmp %r10,%rax
如您所见,相应的功能被编译为本地机器代码。
最后,讨论选项3,AOT。 版本9之前的Java中没有执行执行前的编译AOT。
JDK 9 jaotc中出现了一个新工具-顾名思义,它是Java的AOT编译器。 这个想法是这样的:先运行Java“ javac”编译器,然后运行Java“ jaotc”的AOT编译器,然后照常运行JVM“ java”。 JVM通常执行解释和JIT编译。 但是,如果该函数具有AOT编译的代码,则直接使用它,而不求助于解释或JIT编译。 让我解释一下:您不必运行AOT编译器,它是可选的,如果使用它,则只能在执行之前编译所需的类。
让我们建立一个由AOT编译的
Test::f
组成的库。 不要忘了:要自己执行此操作,您将需要内部版本150+的JDK 9。
jaotc --output=libTest.so Test.class
结果,生成了
libTest.so
,
libTest.so
一个库,其中包含
Test
类中包含的AOT编译的本机功能代码。 您可以查看此库中定义的字符:
nm libTest.so
在我们的结论中,除其他外,将:
0000000000002120 t Test.f()I 00000000000021a0 t Test.<init>()V 00000000000020a0 t Test.main([Ljava/lang/String;)V
因此,我们所有的函数,构造函数,
f
和静态方法
main
都存在于库
libTest.so
。
与相应的“ java”选项一样,在这种情况下该选项可以附带一个文件,为此,jaotc有–compile-commands选项。 JEP 295提供了相关示例,在此不再赘述。
现在运行“ java”,看看是否使用了AOT编译的方法。 如果像以前一样运行“ java”,则将不使用AOT库,这并不奇怪。 要使用此新功能,必须提供
-XX:AOTLibrary
选项,您必须指定:
java -XX:AOTLibrary=./libTest.so Test
您可以指定多个AOT库,以逗号分隔。
该命令的输出与没有
AOTLibrary
“ java”启动时的输出完全相同,因为Test程序的行为完全没有改变。 要检查是否使用了AOT编译的功能,可以添加另一个新选项
-XX:+PrintAOT
。
java -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test
在
Test
程序输出之前,此命令显示以下内容:
9 1 loaded ./libTest.so aot library 99 1 aot[ 1] Test.main([Ljava/lang/String;)V 99 2 aot[ 1] Test.f()I 99 3 aot[ 1] Test.<init>()V
按照计划,将加载AOT库,并使用AOT编译的函数。
如果您有兴趣,可以运行以下命令并检查是否正在进行JIT编译。
java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test
正如预期的那样,不会发生JIT编译,因为Test类中的方法是在执行之前进行编译并作为库提供的。
一个可能的问题是:如果我们提供了本机功能代码,那么JVM如何确定本机代码是否过时/陈旧? 作为最后一个示例,让我们修改函数
f
并将a设置为6。
public int f() throws Exception { int a = 6; return a; }
我这样做只是为了修改类文件。 现在,我们使javac编译并运行与上面相同的命令。
javac Test.java java -client -Xbatch -XX:+PrintCompilation -XX:CompileCommandFile=hotspot_compiler -XX:CompileThreshold=5 -XX:-TieredCompilation -XX:AOTLibrary=./libTest.so -XX:+PrintAOT Test
如您所见,我没有在“ javac”之后运行“ jaotc”,因此AOT库中的代码现在已旧且不正确,并且函数
f
的值为5。
上面的“ java”命令的输出演示:
228 56 b Test::<init> (5 bytes) 229 57 b Test::f (5 bytes)
这意味着在这种情况下功能是动态编译的,因此未使用AOT编译产生的代码。 因此,已在类文件中检测到更改。 使用javac执行编译时,其指纹将输入到类中,并且该类指纹也存储在AOT库中。 由于该类的新指纹与AOT库中存储的指纹不同,因此未使用预先编译的本机代码(AOT)。 这就是我想在执行之前告诉您的最后一个编译选项的全部内容。
在本文中,我试图通过简单的实际示例来解释和说明JVM如何执行Java代码:解释它,动态编译(JIT)或预先编译(AOT)-而且,最后一次机会仅出现在JDK 9中。希望您能学到一些东西新的。