将源代码转换为Java字节码有什么用?


37

如果对于不同的体系结构需要不同的JVM,我无法弄清楚引入这一概念的逻辑是什么。在其他语言中,我们需要用于不同机器的不同编译器,但是在Java中,我们需要不同的JVM,那么引入JVM概念或此额外步骤的逻辑是什么?



12
@gnat:实际上,这不是重复的。这是“源代码与字节代码”,即只是第一个转换。用语言来讲,这是Java语言与Java语言的对比。您的链接将是C ++与Java。
MSalters

2
您想为要添加数字编码以升级的那50种设备模型编写一个简单的字节码解释器,还是为50种不同的硬件编写50种编译器。Java最初是为设备和机械开发的。那是它的强项。在阅读这些答案时,请记住这一点,因为如今Java没有真正的优势(由于解释过程效率低下)。这只是我们继续使用的模型。
大鸭

1
你似乎无法理解,一个虚拟机什么。它是一台机器。可以使用本机代码编译器在硬件中实现(对于JVM,则可以实现)。“虚拟”部分在这里很重要:您实际上是在另一种架构之上模拟该架构。假设我写了一个可以在x86上运行的8088仿真器。您不会将旧的8088代码移植到x86,而只是在仿真平台上运行它。JVM是您像其他任何机器一样针对的机器,不同之处在于JVM 在其他平台之上运行
贾里德·史密斯

7
@TheGreatDuck口译过程?如今,大多数JVM都会对机器代码进行即时编译。更不用说“解释”是当今相当广泛的术语。CPU本身只是将x86代码“解释”为自己的内部微代码,并用于提高效率。一般而言,最新的Intel CPU也非常适合解释器(尽管您当然会找到基准来证明您想要证明的内容)。
a安

Answers:


79

逻辑是JVM字节码比Java源代码简单得多。

可以将编译器视为高度抽象的三个基本部分:解析,语义分析和代码生成。

解析包括读取代码并将其转换为编译器内存中的树表示。语义分析是分析该树,弄清其含义并简化所有高级结构(包括低级结构)的部分。代码生成将简化的树写入到平面输出中。

对于字节码文件,解析阶段被大大简化了,因为它以JIT使用的相同的平面字节流格式编写,而不是递归(树结构)源语言。而且,Java(或其他语言)编译器已经完成了语义分析的许多繁重工作。因此,它要做的就是流读取代码,进行最少的分析和最少的语义分析,然后执行代码生成。

这使JIT的任务执行起来更加简单,因此执行起来也快得多,同时仍保留了高级元数据和语义信息,这使得理论上可以编写单源,跨平台代码成为可能。


7
在applet分发中的其他早期尝试,例如SafeTCL,实际上确实分发了源代码。Java对简单且严格指定的字节码的使用使程序的验证变得更加容易,这就是要解决的难题。诸如p代码之类的字节码已作为解决可移植性问题的解决方案的一部分而闻名(并且ANDF当时可能正在开发中)。
Toby Speight

9
精确地 由于字节码->机器代码步骤,Java启动时间已经成为一个问题。在您的(非平凡的)项目上运行javac,然后想象在每次启动时都执行整个Java->机器代码。
Paul Draper

24
它还有另一个巨大的好处:如果有一天我们都想切换到一种假设的新语言-我们称它为“ Scala”-我们只需要编写一个Scala->字节码编译器,而不是几十个Scala->机器代码编译器。另外,我们免费提供所有JVM特定于平台的优化。
BlueRaja-Danny Pflughoeft

8
JVM字节代码中仍然无法实现某些功能,例如尾部调用优化。我记得这极大地损害了可编译为JVM的功能语言。
JDługosz

8
@JDługosz正确:不幸的是,JVM施加了一些限制/设计习惯用法,尽管如果您来自命令式语言,它们可能是很自然的,但是如果您想为一种从根本上可以工作的语言编写编译器,则会成为相当人为的障碍不同。因此,就将来的语言工作重用而言,我认为LLVM是一个更好的目标–它也有局限性,但它们或多或少地与当前(以及将来可能会出现)处理器的局限性相匹配。
leftaboutabout

27

由于多种原因,各种中间表示在编译器/运行时设计中越来越普遍。

在Java的情况下,最初的首要原因可能是可移植性:Java最初以“一次编写,随处运行”的形式大量销售。尽管您可以通过分发源代码并使用不同的编译器来针对不同的平台来实现此目标,但它有一些缺点:

  • 编译器是复杂的工具,必须了解该语言的所有便捷语法。字节码可以是一种更简单的语言,因为它比人类可读的源代码更接近机器可执行的代码。这意味着:
    • 与执行字节码相比,编译可能会比较慢
    • 针对不同平台的编译器可能最终会产生不同的行为,或者无法跟上语言的变化
    • 为新平台生成编译器比为该平台生成VM(或字节码到本机编译器)要困难得多
  • 分发源代码并不总是可取的;字节码提供了一些针对逆向工程的保护措施(尽管除非有意混淆,否则反编译仍然相当容易)

中间表示的其他优点包括:

  • 优化,可以在字节码中发现模式并将其编译为更快的等效项,甚至可以在程序运行时针对特殊情况进行优化(使用“ JIT”或“ Just In Time”编译器)
  • 同一虚拟机中多种语言之间的互操作性;这已在JVM(例如Scala)中流行,并且是.net框架的明确目标。

1
Java还面向嵌入式系统。在这样的系统中,硬件具有一些内存和cpu约束。
Laiv

可以通过以下方式开发编译器:首先将Java源代码编译为字节代码,然后再将字节代码编译为机器代码?它会消除您提到的大多数缺点吗?
Sher10ck '18年

@ Sher10ck是的,很可能AFAIK可以编写将JVM字节码静态转换为特定体系结构的机器指令的编译器。但这只有在性能提高到足以胜过分发者的额外精力或用户首次使用的额外时间时才有意义。低功耗嵌入式系统可能会受益。调整良好的JIT可能会更好地实现现代PC下载并运行许多不同程序的目的。我认为Android朝着这个方向发展,但是不知道细节。
IMSoP

8

听起来您想知道为什么我们不仅仅分发源代码。让我解决这个问题:为什么我们不只是分发机器代码?

显然,这里的答案是Java,从设计上讲,并不假定它知道在什么机器上运行代码。它可能是台式机,超级计算机,电话或两者之间的任何东西。Java为本地JVM编译器留出了执行其操作的空间。除了提高代码的可移植性之外,这还具有一个很好的好处,即允许编译器执行某些操作,例如利用特定于机器的优化(如果存在)或至少产生不起作用的代码。喜欢的东西SSE指令或硬件加速,可以使用只支持他们的机器。

如此看来,在原始源代码上使用字节码的原因就更清楚了。尽可能接近原始机器语言可以使我们实现或部分实现机器代码的某些好处,例如:

  • 由于已经完成了一些编译和分析,因此启动时间更快。
  • 安全性,因为字节码格式具有用于对分发文件进行签名的内置机制(源可以按惯例进行此操作,但是实现该功能的机制并不是字节码内置的方式)。

请注意,我没有提到执行速度更快。从理论上讲,源代码和字节代码都可以或可以完全编译为同一机器代码以实际执行。

另外,字节码允许对机器码进行一些改进。当然,我前面已经提到了平台独立性和特定于硬件的优化,但是也有一些服务,例如服务JVM编译器以根据旧代码产生新的执行路径。这可能是为了修补安全问题,或者是发现新的优化方法,还是要利用新的硬件说明。在实践中,很少会看到这种大变化,因为它可以暴露错误,但是有可能,而且这种情况总是以小小的方式发生。


8

这里似乎至少有两个不同的可能问题。通常,实际上是关于编译器的,而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字节码

Java字节码与P代码非常相似。这基本上是一台相当简单的机器的说明。该机器旨在作为现有机器的抽象,因此将其快速转换为几乎任何特定目标都相当容易。早期的翻译很重要,因为最初的目的是解释字节码,就像P-System所做的一样(是的,这正是早期实现的工作方式)。

长处

Java字节码很容易为编译器前端生成。例如,如果您有一个相当典型的表示表达式的树,则遍历该树并直接从每个节点上找到的代码直接生成代码通常很容易。

Java字节码非常紧凑-在大多数情况下,比大多数典型处理器(尤其是大多数RISC处理器,例如Sun在设计Java时出售的SPARC)的源代码或机器代码要紧凑得多。这在当时尤其重要,因为Java的主要目的是支持小程序-嵌入在执行之前要下载的网页中的代码-当时大多数人通过调制解调器通过电话线访问我们的时间约为28.8。千比特每秒(尽管,当然,仍有相当多的人在使用较旧,较慢的调制解调器)。

弱点

Java字节码的主要缺点是它们没有特别的表现力。尽管它们可以很好地表达Java中存在的概念,但是对于表达不属于Java的概念却不能很好地发挥作用。同样,尽管在大多数计算机上执行字节代码很容易,但是要想充分利用任何特定计算机的方式,则要困难得多。

例如,如果您确实确实想优化Java字节码,那么您通常会进行一些反向工程,以将它们从诸如表示形式的机器代码向后翻译,然后将其转换回SSA指令(或类似的东西)2。然后,您可以操纵SSA指令进行优化,然后从那里转换为针对您真正关心的体系结构的对象。但是,即使这个过程相当复杂,对于Java而言,某些陌生的概念也很难表达,因此很难将某些源语言转换成可以在大多数典型机器上最佳运行(甚至接近)的机器代码。

摘要

如果您询问为什么一般使用中间表示,则两个主要因素是:

  1. 将O(N * M)问题简化为O(N + M)问题,然后
  2. 将问题分解为更易于管理的部分。

如果您要询问Java字节码的细节,以及为什么他们选择这种特定的表示形式而不是其他表示形式,那么我想说的答案主要是回到最初的意图和当时网络的局限性,导致以下优先事项:

  1. 紧凑的表示形式。
  2. 快速简便地解码和执行。
  3. 在大多数普通机器上实现快速简便。

能够代表多种语言或在各种目标上实现最佳执行的优先级要低得多(如果根本不考虑它们)。


  1. 那么,为什么大多数人都忘记了P系统?主要是定价情况。P-system在Apple II,Commodore SuperPets等产品上的销售相当不错。当IBM PC出现时,P-system是受支持的OS,但是MS-DOS的成本更低(从大多数人的角度来看,基本上是免费提供的),很快就有更多的程序可用,因为这是Microsoft和IBM(以及其他公司)写的。
  2. 例如,这就是Soot的工作方式。

与Web小程序非常接近:最初的目的是将代码分发到设备(机顶盒...),就像RPC分发函数调用和CORBA分发对象一样。
ninjalj

2
这是一个很好的答案,并且很好地了解了不同的中间表示形式如何做出不同的取舍。:)
IMSoP '17

@ninjalj:那真的是橡树。当它变成Java时,我相信机顶盒(和类似的)想法已经被搁置了(尽管我是第一个承认有充分的论据认为Oak和Java是同一件事)。
杰里·科芬

@TobySpeight:是的,表达可能更适合那里。谢谢。
杰里·科芬

0

除了其他人指出的优点外,字节码还小得多,因此在目标环境中更易于分发和更新并占用更少的空间。这在空间受限的环境中尤其重要。

它还使保护受版权保护的源代码更加容易。


2
Java(和.NET)字节码非常容易转换成清晰易读的源代码,以至于有些产品可以处理名称,有时还需要其他信息来使其变得更难-有时我们也需要对JavaScript进行一些操作以使其更小,因为我们现在也许设置用于Web浏览器的字节码。
LnxPrgr3

0

感觉是,从字节码到机器码的编译比及时地将原始代码解释为机器码要快。但是我们需要进行解释以使我们的应用程序跨平台,因为我们希望在每个平台上使用原始代码,而无需进行任何更改,也无需任何准备(编译)。因此,首先javac将源代码编译为字节码,然后我们可以在任何地方运行该字节码,并且Java虚拟机将其解释为更快的机器码。答案:节省时间。


0

最初,JVM是一个纯解释器。如果您要翻译的语言尽可能简单,那么您将获得性能最好的翻译。这就是字节码的目标:向运行时环境提供有效的可解释输入。根据其性能判断,这一单一决定使Java更接近于编译语言,而不是解释语言。

直到后来,当解释JVM的性能仍然明显下降时,人们才投入精力来创建性能良好的即时编译器。这在某种程度上弥合了C和C ++等更快的语言的差距。(不过,仍然存在一些Java固有的速度问题,因此您可能永远无法获得性能和编写良好的C代码一样好的Java环境。)

当然,有了即时编译技术,我们可以回到实际分发源代码,然后将其及时编译为机器代码。但是,这将严重降低启动性能,直到编译代码的所有相关部分为止。字节码在这里仍然是一个重要的帮助,因为它比等效的Java代码解析起来简单得多。


下选民请介意解释原因吗?
cmaster

-5

文本源代码是一种旨在易于人类阅读和修改的结构。

字节码是一种旨在易于由机器读取和执行的结构。

由于所有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”的成员。


2
这些字节码“示例”是人类可读的。那根本不是字节码。这具有误导性,也无法解决所提出的问题。
通配符

@Wildcard您可能已经错过了这个由人类阅读的论坛。这就是为什么我将内容以易于阅读的形式放置。鉴于该论坛是关于软件工程的,因此要求读者了解简单抽象的概念并不需要太多。
彼得

可读形式是源代码,而不是字节码。您将使用预先计算的表达式而 不是字节码来说明代码。我没有错过这是一个人类可读的论坛:您是一个批评其他回答者的人,因为它不包含任何字节码示例,不是我。所以您说:“我注意到还没有任何示例”,然后继续给出完全不说明字节码的示例。这仍然根本没有解决这个问题。重读问题。
通配符
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.