Java为什么解释执行时不直接解释源码?

论坛 期权论坛 期权     
匿名用户1024   2022-2-9 13:52   10176   5
Java源码为什么会经过中间步骤转换为字节码,这样不是增加工作量吗?直接解释源代码一样跨平台。
为什么不在解释运行时直接解释源代码,而是字节码。
分享到 :
0 人收藏

5 个回复

倒序浏览
2#
有关回应  16级独孤 | 2022-2-9 13:52:11
题主的问题是:
Java源码为什么会经过中间步骤转换为字节码,这样不是增加工作量吗?直接解释源代码一样跨平台。
其实题主可能是出于这个角度问的:
为什么执行Java程序必须先用Java源码编译器(例如javac)编译为Java字节码,然后在用JVM执行Java字节码;就不能直接输入源码就得到执行结果么?

答案是:当然可以。分离的编译-执行模型是可以封装的。
完全可以写一个程序接受Java源码输入,内部悄悄调用javac把源码编译为字节码,然后交给JVM去执行得到结果。事实上通过Java SE 6开始提供的Java Compiler API非常容易在Java里实现这点,都不必调用外部的“javac”命令。
像Ruby(CRuby 1.9或以上)、Python(CPython),内部都是先把源码编译到字节码然后再解释执行的,但从用户的角度看就只有一个ruby / python命令,并没有分离的编译-执行步骤,看似是“直接解释执行源码”。

===========================================

这是一个“语言处理器”(language processor)的话题。
用于执行编程语言的语言处理器,从解释器到编译器这两个极端之间有整个系列的选择。

Java作为一种“古老”的编程语言,实现其执行的语言处理器也有全系列可选。

其中一个是DynamicJava,它就是一种源码解释器,直接在Java源码上解释执行而不编译到Java字节码再解释执行。具体说它是先把Java源码通过词法+语法分析转换为抽象语法树(AST)之后再在抽象语法树上做解释执行的:TreeInterpreter

从Java 9开始,Oracle JDK / OpenJDK将自带一个“jshell”命令,同样可以直接解释执行Java源码。详细请参考:
从用户的角度看,源码进去,执行结果就出来了,中间经过了怎样的步骤其实都不重要 ;-)

回到“全系列”的选择,那到底有些什么选择呢?
我们可以从一个比较简单的编译器的处理步骤看起:
  1. 编译流程:  源码 [字符流]- 词法分析 -> 单词(token)流- 语法分析 -> 语法树 / 抽象语法树- 语义分析 -> 标注了属性的抽象语法树- 代码生成 -> 目标代码执行流程:  目标代码- 操作系统/硬件 -> 执行结果
复制代码
这描述的是一个分离的编译-执行流程:编译生成目标代码,目标代码持久化到例如磁盘上,然后执行时把目标代码再加载起来并执行出结果。
(注:这里假定目标代码是硬件可以直接执行的机器码)

在上面的流程中,我们可以从后向前逐步把处理融合起来。每融合一个处理步骤,在“执行”之前的处理部分看起来就更少更简单了一些,但在“执行”时要做的冗余动作就更多了一些。

例如说我们可以不要求用分离的编译-执行流程,而是直接在编译出目标代码之后让目标代码直接放在内存里,然后直接让硬件开始执行目标代码:
  1. 编译+执行流程:  源码 [字符流]- 词法分析 -> 单词(token)流- 语法分析 -> 语法树 / 抽象语法树- 语义分析 -> 标注了属性的抽象语法树- 代码生成 -> 目标代码- 操作系统/硬件 -> 执行结果
复制代码
与之前的分离流程相比,这里从输入源码到得到执行结果只有一步,从使用角度看似乎简单了一些,但同时也意味着每次重新执行同样的源码都必须重新经过从源码到生成目标代码之间的编译流程,冗余变多了。

然后我们可以进一步从后向前融合,不生成目标代码,而是让程序维持在一种中间形式上就开始解释执行。例如说:
  1. 编译+解释执行流程:  源码 [字符流]- 词法分析 -> 单词(token)流- 语法分析 -> 语法树 / 抽象语法树- 语义分析 -> 标注了属性的抽象语法树- 不做类型检查的抽象语法树解释器 -> 执行结果
复制代码
这里我们通过实现一个能在硬件上执行的抽象语法树解释器(AST interpreter,或者就叫tree interpreter)来实现源程序的执行。
要留意的是:由于在解释执行前做了语义分析(其中包括但不限于类型检查),我们可以相信输入到解释器的抽象语法树的类型是正确的,所以解释器里不必重复做类型检查。
其它可能在语义分析阶段做的处理诸如:
  • 变量的确定性赋值:变量必须在使用前先得到初始赋值;
  • 变量的确定性不重复赋值:不可变变量(例如Java的final变量)最多只能被赋值一次
  • 控制流的正确性校验:例如Java的continue语句只能用在循环体内、continue的跳转标签只能向更外围作用域而不能向更深的嵌套作用域跳转,等等;
在解释执行之前做好这些分析,就意味着在解释执行过程中完全不必关心这些检查,因而解释执行的效率就可以更高。

然后可以进一步去掉解释执行前的语义分析,变为:
  1. 编译+解释执行流程:  源码 [字符流]- 词法分析 -> 单词(token)流- 语法分析 -> 语法树 / 抽象语法树- 需要做类型检查的抽象语法树解释器 -> 执行结果
复制代码
没有了解释执行前的语义分析,要维持语言的语义正确,就必须在解释执行过程中融入语义分析本来应该完成的动作。例如:
  • 在看到一个“赋值”动作时,必须检查赋值目标(“左手边”)
    • 在作用域内是否存在
    • 类型是否匹配
    • 是否是final变量并且已经得到过赋值
    • 等等
  • 在运行时必须维护一个“循环嵌套栈”,在执行“continue语句”时必须检查当前是否在循环里,并且要动态查找continue的跳转目标
  • 等等
这些解释执行时做的语义分析的结果都不会被保存下来,所以多次执行到同一块代码时就得重复做这些分析。
这样,同样是在抽象语法树上解释执行,这个解释器就比上一个版本的解释器要重复做更多处理,因而会更复杂以及更慢。

我们可以进一步把语法分析也融合到解释执行中,变为:
  1. 编译+解释执行流程:  源码 [字符流]- 词法分析 -> 单词(token)流- 需要做语法分析+类型检查的单词流解释器 -> 执行结果
复制代码
此时解释器就不是在抽象语法树,而是在单词流上做解释执行了。为了保证我们只接受符合语法规则的程序,我们还是得做语法分析——只是把它融合到了解释器里而已。
与上一个版本的解释器最大的不同时,这个版本在解释器不会保留语法树/抽象语法树,所以解释器会一边做语法分析一边解释执行,如果多次执行同一块代码就得重复做语法分析。

最后,我们可以把词法分析也融合到解释执行中:
  1. 解释执行流程:  源码 [字符流]- 需要做词法分析+语法分析+类型检查的字符流解释器 -> 执行结果
复制代码
有了前面的讲解,相信这一步是怎么回事不必多说了。

要实现一门编程语言,上面说的所有可能性都可以实现“执行”这一目标,但是从运行效率上看明显大有不同。

我以前做的一套演讲稿里,第6到第9页就是讲这个话题的:http://www.valleytalk.org/wp-content/uploads/2011/05/Java_Program_in_Action_20110727.pdf

使用解释器实现的编程语言实现里,通常:
  • 至少会在解释执行前做完语法分析,然后通过树解释器来实现解释执行;
  • 兼顾易于实现、跨平台、执行效率这几点,会选择使用字节码解释器实现解释执行。
在树解释器与字节码解释器中也各自有许多不同的变种,这里就不多展开说了。

在这两大类解释器中的取舍,请参考另一个回答:为什么大多数解释器都将AST转化成字节码再用虚拟机执行,而不是直接解释AST? - RednaxelaFX 的回答

碰到这种话题我总是忍不住想放俩老传送门:
虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩
[讨论]CPython能否用V8的方式优化性能
3#
有关回应  16级独孤 | 2022-2-9 13:52:12
  • 字节码更便于虚拟机读取,不用在解析字符串,所以运行速度比直接解析源代码快。
  • 语法是会变的,而源代码中没有版本信息,而字节码中不但有版本信息,还可以经由编译过程抹平一些语言层面的变化(即语言语法虽然有变化,但字节码依然遵照原来的规则即可)。
  • 字节码也可以由其他语言生成,如GroovyClojureScala。需要注意的事,既然这些语言可以编译成字节码,也就可以被Java或其他JVM语言调用。
  • 然而归根到底,没有必须这么设计的绝对理由,很多事物之所以会这样,只是因为被设计成这样。
4#
有关回应  16级独孤 | 2022-2-9 13:52:13
理论上,完全可以直接解释源码,这样也可以跨平台。而引入字节码有额外的好处:
  • 直接执行字节码,比解释源码再执行,会更快。
  • 生成字节码过程中,编译器可以预先作语法错误或者安全性方面的检查,出错机会更少。
  • 字节码比源码更加紧凑,文件尺寸更小,方便网络传输。
  • 有些嵌入设备,不够资源跑起完整的编译器,这些设备只需要嵌入一个小巧的JVM就行了,在额外的平台上编译源码。
  • 字节码不一定非要java源码生成,其它一些语言比如scala也可以编译生成字节码。这样其它语言就可以利用上经过多年发展的JVM。
5#
有关回应  16级独孤 | 2022-2-9 13:52:14
我就补充点R大的回复。。

Interpreation the source code is OK, but might be slow in many places because of absent of any optimization.  By translation, the generate code can be somewhat optimized or simplified.

Norammly, there are a lot of mode for interpretation.
1) Interprete source code directly.
2) Interprete IR, eg. bytecode.
3) between 1) and 2), e.g.,  interprete AST tree.
4) Translate AST to native code and execute.
5) ..
Many interpreters would mix above four modes. For example,  JIT compilation for Java/JavaScript which mixes of bytecode interpretation and native code execution in many projects.

The decison is made for the performance/memory/ factors. For example, the interpreter in CRuby under 1.9 is AST interpreter that does not generate bytecode. However, it generates bytecode since V1.9 for the performance purpose. Similarly, the JRuby+Truffle take the idea of early CRuby. The interpreter walks on the AST tree and then Truffle(Graal) compiles the code and execution.  This is also performance purpose.
6#
有关回应  16级独孤 | 2022-2-9 13:52:15
  • Java严格说来是“半解释半编译”型的语言
  • Java代码首先由javac编译成字节码(ByteCode)。ByteCode是JVM唯一能够识别的指令,JVM将ByteCode翻译成真正能够执行的机器码
  • 字节码的规范由JVM规范(The Java Virtual Machine Specification)定义,JVM在不同的硬件平台上需要有不同实现,以达到所谓“一次编写,到处运行”的目标。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:136515
帖子:27303
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP