如果对于不同的体系结构需要不同的JVM,我无法弄清楚引入这一概念的逻辑是什么。在其他语言中,我们需要用于不同机器的不同编译器,但是在Java中,我们需要不同的JVM,那么引入JVM概念或此额外步骤的逻辑是什么?
如果对于不同的体系结构需要不同的JVM,我无法弄清楚引入这一概念的逻辑是什么。在其他语言中,我们需要用于不同机器的不同编译器,但是在Java中,我们需要不同的JVM,那么引入JVM概念或此额外步骤的逻辑是什么?
Answers:
逻辑是JVM字节码比Java源代码简单得多。
可以将编译器视为高度抽象的三个基本部分:解析,语义分析和代码生成。
解析包括读取代码并将其转换为编译器内存中的树表示。语义分析是分析该树,弄清其含义并简化所有高级结构(包括低级结构)的部分。代码生成将简化的树写入到平面输出中。
对于字节码文件,解析阶段被大大简化了,因为它以JIT使用的相同的平面字节流格式编写,而不是递归(树结构)源语言。而且,Java(或其他语言)编译器已经完成了语义分析的许多繁重工作。因此,它要做的就是流读取代码,进行最少的分析和最少的语义分析,然后执行代码生成。
这使JIT的任务执行起来更加简单,因此执行起来也快得多,同时仍保留了高级元数据和语义信息,这使得理论上可以编写单源,跨平台代码成为可能。
由于多种原因,各种中间表示在编译器/运行时设计中越来越普遍。
在Java的情况下,最初的首要原因可能是可移植性:Java最初以“一次编写,随处运行”的形式大量销售。尽管您可以通过分发源代码并使用不同的编译器来针对不同的平台来实现此目标,但它有一些缺点:
中间表示的其他优点包括:
听起来您想知道为什么我们不仅仅分发源代码。让我解决这个问题:为什么我们不只是分发机器代码?
显然,这里的答案是Java,从设计上讲,并不假定它知道在什么机器上运行代码。它可能是台式机,超级计算机,电话或两者之间的任何东西。Java为本地JVM编译器留出了执行其操作的空间。除了提高代码的可移植性之外,这还具有一个很好的好处,即允许编译器执行某些操作,例如利用特定于机器的优化(如果存在)或至少产生不起作用的代码。喜欢的东西SSE指令或硬件加速,可以使用只支持他们的机器。
如此看来,在原始源代码上使用字节码的原因就更清楚了。尽可能接近原始机器语言可以使我们实现或部分实现机器代码的某些好处,例如:
请注意,我没有提到执行速度更快。从理论上讲,源代码和字节代码都可以或可以完全编译为同一机器代码以实际执行。
另外,字节码允许对机器码进行一些改进。当然,我前面已经提到了平台独立性和特定于硬件的优化,但是也有一些服务,例如服务JVM编译器以根据旧代码产生新的执行路径。这可能是为了修补安全问题,或者是发现新的优化方法,还是要利用新的硬件说明。在实践中,很少会看到这种大变化,因为它可以暴露错误,但是有可能,而且这种情况总是以小小的方式发生。
这里似乎至少有两个不同的可能问题。通常,实际上是关于编译器的,而Java基本上只是这种类型的示例。另一种是Java特定于它使用的特定字节码。
首先让我们考虑一个普遍的问题:为什么编译器在编译源代码以在某些特定处理器上运行的过程中使用一些中间表示形式?
答案很简单:它将O(N * M)问题转换为O(N + M)问题。
如果给我们N种源语言和M种目标,并且每个编译器都是完全独立的,那么我们需要N * M个编译器将所有这些源语言翻译成所有这些目标(其中“目标”类似于处理器和操作系统)。
但是,如果所有这些编译器都同意一个通用的中间表示形式,那么我们可以有N个编译器前端将源语言转换为中间表示形式,M个编译器后端将中间语言表示转换为适合特定目标的内容。
更好的是,它将问题分成两个或更多或更少的专有域。知道/关心语言设计,解析等工作的人可以专注于编译器前端,而知道指令集,处理器设计等工作的人可以专注于后端。
因此,例如,给定类似LLVM的东西,我们有许多用于各种不同语言的前端。我们也有许多不同处理器的后端。语言专家可以为其语言编写新的前端,并快速支持许多目标。处理器人员可以为目标编写新的后端,而无需处理语言设计,解析等。
将编译器分为前端和后端,并用中间表示形式在两者之间进行通信并不是Java的原始设计。长期以来,这一直是相当普遍的做法(因为在Java出现之前就已经很早了)。
就Java在这方面添加的任何新内容而言,它在分发模型中。特别是,即使很长时间以来,编译器在内部一直被分为前端和后端,但它们通常作为单个产品分发。例如,如果您购买了Microsoft C编译器,则在内部它具有一个“ C1”和一个“ C2”,分别是前端和后端-但是您购买的仅仅是“ Microsoft C”,其中包括件(带有“编译器驱动程序”,用于协调两者之间的操作)。即使编译器分为两部分,对于使用该编译器的普通开发人员来说,它也只是一件事,即从源代码转换为目标代码,而在两者之间看不到任何东西。
相反,Java在Java开发工具包中分发了前端,而在Java虚拟机中分发了后端。每个Java用户都有一个编译器后端,可以针对他正在使用的任何系统。Java开发人员以中间格式分发代码,因此,当用户加载代码时,JVM会执行在其特定计算机上执行代码所需的一切。
请注意,这种分布模型也不是全新的。仅举例来说,UCSD P系统的工作方式类似:编译器前端生成P代码,并且P系统的每个副本都包含一个虚拟机,该虚拟机执行了在该特定目标1上执行P代码所需的操作。
Java字节码与P代码非常相似。这基本上是一台相当简单的机器的说明。该机器旨在作为现有机器的抽象,因此将其快速转换为几乎任何特定目标都相当容易。早期的翻译很重要,因为最初的目的是解释字节码,就像P-System所做的一样(是的,这正是早期实现的工作方式)。
Java字节码很容易为编译器前端生成。例如,如果您有一个相当典型的表示表达式的树,则遍历该树并直接从每个节点上找到的代码直接生成代码通常很容易。
Java字节码非常紧凑-在大多数情况下,比大多数典型处理器(尤其是大多数RISC处理器,例如Sun在设计Java时出售的SPARC)的源代码或机器代码要紧凑得多。这在当时尤其重要,因为Java的主要目的是支持小程序-嵌入在执行之前要下载的网页中的代码-当时大多数人通过调制解调器通过电话线访问我们的时间约为28.8。千比特每秒(尽管,当然,仍有相当多的人在使用较旧,较慢的调制解调器)。
Java字节码的主要缺点是它们没有特别的表现力。尽管它们可以很好地表达Java中存在的概念,但是对于表达不属于Java的概念却不能很好地发挥作用。同样,尽管在大多数计算机上执行字节代码很容易,但是要想充分利用任何特定计算机的方式,则要困难得多。
例如,如果您确实确实想优化Java字节码,那么您通常会进行一些反向工程,以将它们从诸如表示形式的机器代码向后翻译,然后将其转换回SSA指令(或类似的东西)2。然后,您可以操纵SSA指令进行优化,然后从那里转换为针对您真正关心的体系结构的对象。但是,即使这个过程相当复杂,对于Java而言,某些陌生的概念也很难表达,因此很难将某些源语言转换成可以在大多数典型机器上最佳运行(甚至接近)的机器代码。
如果您询问为什么一般使用中间表示,则两个主要因素是:
如果您要询问Java字节码的细节,以及为什么他们选择这种特定的表示形式而不是其他表示形式,那么我想说的答案主要是回到最初的意图和当时网络的局限性,导致以下优先事项:
能够代表多种语言或在各种目标上实现最佳执行的优先级要低得多(如果根本不考虑它们)。
除了其他人指出的优点外,字节码还小得多,因此在目标环境中更易于分发和更新并占用更少的空间。这在空间受限的环境中尤其重要。
它还使保护受版权保护的源代码更加容易。
感觉是,从字节码到机器码的编译比及时地将原始代码解释为机器码要快。但是我们需要进行解释以使我们的应用程序跨平台,因为我们希望在每个平台上使用原始代码,而无需进行任何更改,也无需任何准备(编译)。因此,首先javac将源代码编译为字节码,然后我们可以在任何地方运行该字节码,并且Java虚拟机将其解释为更快的机器码。答案:节省时间。
最初,JVM是一个纯解释器。如果您要翻译的语言尽可能简单,那么您将获得性能最好的翻译。这就是字节码的目标:向运行时环境提供有效的可解释输入。根据其性能判断,这一单一决定使Java更接近于编译语言,而不是解释语言。
直到后来,当解释JVM的性能仍然明显下降时,人们才投入精力来创建性能良好的即时编译器。这在某种程度上弥合了C和C ++等更快的语言的差距。(不过,仍然存在一些Java固有的速度问题,因此您可能永远无法获得性能和编写良好的C代码一样好的Java环境。)
当然,有了即时编译技术,我们可以回到实际分发源代码,然后将其及时编译为机器代码。但是,这将严重降低启动性能,直到编译代码的所有相关部分为止。字节码在这里仍然是一个重要的帮助,因为它比等效的Java代码解析起来简单得多。
文本源代码是一种旨在易于人类阅读和修改的结构。
字节码是一种旨在易于由机器读取和执行的结构。
由于所有JVM处理代码的操作都可以读取并执行,因此字节代码更适合JVM使用。
我注意到还没有任何示例。愚蠢的伪示例:
//Source code
i += 1 + 5 * 2 + x;
// Byte code
i += 11, i += x
____
//Source code
i = sin(1);
// Byte code
i = 0.8414709848
_____
//Source code
i = sin(x)^2+cos(x)^2;
// Byte code (actually that one isn't true)
i = 1
当然,字节码不仅仅涉及优化。它的很大一部分是关于能够执行代码而不必关心复杂的规则,例如当方法引用“ foo”时,检查类是否在文件中更下方的位置包含名为“ foo”的成员。