传统解释器、编译器和JIT编译器/解释器的澄清

4

我正在学习Java,以下内容对我来说有些困惑。我的理解是:

  • Java编译器 → Java编译器只是将.java程序转换为.class文件,也就是将源代码转换成字节码(它是虚拟机(JVM)的操作码列表,这使得Java具备了平台无关性)。

  • Java解释器 → 仅仅“解释”代码,并且不会将其转换为本地机器代码。它会按照命令逐条执行每个字节码指令,无论相同的指令出现了多少次。这就是为什么它很慢,而Java引入了JIT概念。

  • JIT编译器 → 这也是在执行时发挥作用的。JIT编译器能够通过缓存已经被翻译的代码块的结果来提高性能,与简单地重新评估字节码中的每一行或操作数相比。

现在我有几个问题:

  1. 由于我的物理处理器只理解本地机器代码,使用JVM的解释器如何执行Java程序?解释器不会将字节码转换为本地机器代码。除非有人将机器代码放入内存中,否则物理处理器将无法执行它。

  2. 假设解释器也能将字节码转换为本地机器代码,那么“带缓存的代码块执行(JIT)和逐行执行(解释器)”是唯一区分JIT和解释器的东西吗?

  3. 如果在执行时,JIT编译器将字节码转换为本地机器代码(用于执行程序),为什么Java不使用预先编译?在生成基于JVM的字节码之后(反过来使Java具备平台无关性),我们可以将其带到要执行它的目标机器上,并将其转换为本地机器代码(与C编译的情况相同,创建一个.exe.out文件)。这是可能的,因为每个系统上都有特定的JVM。这比使用JIT编译更快,因为它需要一些时间来编译和加载程序。通过只分发字节码(在最终从字节码到机器代码的转换之前生成),它仍将是平台无关的。

2个回答

4
免责声明:请将这些内容视为过于简单化的内容。
1:您是正确的,计算机本身并不理解代码,这就是为什么需要JVM本身。假设 XY 表示“添加堆栈上的前两个元素并推送结果”。然后,JVM将被实现为以下方式:
for(byte bytecode : codeToExecute) {
    if (bytecode == XX) {
        // ...do stuff...
    } else if (bytecode == XY) {
        int a = pop();
        int b = pop();
        push(a+b);
    } else if (bytecode == XZ) {
        // ...do stuff...
    } // ... and so on for each possible instruction ...
}

JVM已经在计算机的本地机器码中实现了每个单独的指令,并且基本上查找每个字节码块以了解如何执行它。通过JIT编译代码,可以通过省略此解释(即查找每个指令应该如何处理)来实现大幅加速。这是优化的一部分。
JIT并不真正运行代码;所有内容仍在JVM内部运行。基本上,当适当时,JIT将一个字节码块转换为机器码。当JVM遇到它时,它会认为“哦,嘿,这已经是机器码了!太棒了,现在我不必仔细检查每个字节,因为CPU自己就能理解它了!我只需将其传送过去,一切都会神奇地自动工作!”
是的,可以以这种方式预编译代码,以避免早期解释和JIT的开销。但是,这样做会失去非常有价值的东西。您看,当JVM解释代码时,它还会保留有关所有内容的统计信息。然后,当它JIT代码时,它知道不同部分使用的频率,使它能够在重要部分进行优化,使常见的东西更快,而牺牲罕见的东西,从而产生整体性能提升。

  1. 这里的JVM是指解释器还是一些其他组件的组合?
  2. 你的意思是JIT可以使解释器的任务更快,但在JIT之后仍然需要解释吗?
  3. JIT编译器如何进行更好的优化?传统的(B代码到m代码)编译器可以在执行之前使用完整的代码进行更好的优化。(在C/C++的情况下,GCC可以做出非常好的优化)
- Deepankar Singh
  1. JVM基本上意味着“运行Java程序的东西”,其中包括解释和其他内容。
  2. 是和否。即使类MyClass已经被JIT编译,程序的其余部分仍可能被解释。因此,JVM仍然需要处理解释和JIT编译代码之间的转换。
  3. 我没有任何具体例子。我忘记了收集哪些数据,所以很难想出一个假设的情况。
- BambooleanLogic
谢谢你的帮助。但我仍然好奇为什么采用了JIT概念,如果有传统的编译和优化方式可以支持可移植性和良好的性能。 - Deepankar Singh
@DeepankarSingh并没有。编译后的代码总是比解释后的代码更快。HotSpot JVM 的目的是找到花费时间的解释代码位,并仅对其进行编译。 - user207421

1
  1. 最终,在解释器中运行与字节码指令等效的机器代码,只是更加间接。可以把它想象成钢琴。CPU就像自动演奏钢琴。编译器会在纸条上打孔,然后你可以在钢琴上演奏这个纸条。使用解释器本质上就是在钢琴前面放置一个类似人类的机器,他读取乐谱并按下钢琴键。最后,相同的和弦响起,但多了一层间接性。
  2. 是的,基本如此。
  3. 正如你所提到的,Java被吹嘘为“编写一次,随处运行”。因此,字节码是实现该承诺的重要特征。它之所以不会在计算机上编译,是因为编译速度较慢。通常情况下,编译发生在软件开发者那里。如果你启动的每个Java应用程序都是第一次编译,那么你将需要等待一段时间。
  4. 由于大多数程序并没有完全运行(有分支未被执行,未选择的菜单项等),通常编译需要的部分即时编译,这样更快。这也节省了磁盘空间,否则你的计算机上将有两份每个程序的副本。
那么,你并不是第一个对此感到困惑的人。实际上,一些人会用Java编写程序,但使用编译器来生成可执行文件。Java(转换为本地代码的)编译器确实存在,只是不常见,因为很多人在这个阶段使用可移植的C或C++。

  1. 你的第一个比喻我没太听懂。
  2. 如果它们几乎相同,那么 JVM 在 JIT 编译代码后就不需要进行任何解释了吗?
  3. 虽然传统的代码编译速度较慢(比 JIT 更慢),但一旦编译完成,可执行文件可以运行多次,比每次 JIT 编译代码要快得多。因此,具有双重副本和一次编译将更具优势。
- Deepankar Singh
“Player piano(播放钢琴)”是自动钢琴。你可以在一些西部电影(那种有牛仔的,不是通常的西部电影)中看到它。它里面有一卷类似打孔纸带的卷轴,可以让它演奏某首曲子。 - uliwitness
@DeepankarSingh 像音乐盒一样。 - Koray Tugay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接