【个人思考】 Java为什么解释执行时不直接解释源码?

2024-09-06 00:52

本文主要是介绍【个人思考】 Java为什么解释执行时不直接解释源码?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

起因

最近学习JVM,产生一个问题:Java为什么解释执行时不直接解释源码?

众所周知,Java 字节码是跨平台的,因此 Java 才能一次编译处处运行。但是,Java 源码本身也是跨平台的啊,为什么不可以省略编译为字节码这一步,直接将源码运行在虚拟机上?如果是效率问题,可不可以在设计 Java 语言的时候解决?

对于此问题,大部分回答诸如:“采用字节码的好处主要包括跨平台性、安全性、性能优化、易于维护以及支持动态性等。这些优势使得字节码成为现代编程语言中不可或缺的一部分。”等回答,完全答非所问

经过长时间搜索,找到一些答案,个人感觉回答的内容较为合理。

搜索到的感觉比较靠谱的回答

一、字节码相对于源码的一些优势

  • 字节码解析速度更快
  • 字节码size压缩的更小
  • 字节码格式版本更稳定,语法改变大部分情况下不影响字节码格式
  • 字节码可以在编译时提前做编译优化,节省运行时编译优化时间
  • 字节码能保护源码,增加反编译成本
  • 字节码能支持多语言,在其平台上开发新语言更容易(字节码不一定非要java源码生成,其它一些语言比如scala也可以编译生成字节码。这样其它语言就可以利用上经过多年发展的JVM。)
  • 编译器前端和虚拟机可以独立开发,互不影响。有中间格式也更容易debug

二、xx上一个高赞回答

为什么执行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源码。详细请参考:

  • JEP 222: jshell: The Java Shell (Read-Eval-Print Loop)
  • OpenJDK: Kulla
  • JShell and REPL in Java 9 (The Java Source)
  • Java 9 Early Access: A Hands-on Session with JShell

从用户的角度看,源码进去,执行结果就出来了,中间经过了怎样的步骤其实都不重要 ;-)

回到“全系列”的选择,那到底有些什么选择呢?

我们可以从一个比较简单的编译器的处理步骤看起:

编译流程:源码 [字符流]
- 词法分析 -> 单词(token)流
- 语法分析 -> 语法树 / 抽象语法树
- 语义分析 -> 标注了属性的抽象语法树
- 代码生成 -> 目标代码执行流程:目标代码
- 操作系统/硬件 -> 执行结果

这描述的是一个分离的编译-执行流程:编译生成目标代码,目标代码持久化到例如磁盘上,然后执行时把目标代码再加载起来并执行出结果。

(注:这里假定目标代码是硬件可以直接执行的机器码)

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

例如说我们可以不要求用分离的编译-执行流程,而是直接在编译出目标代码之后让目标代码直接放在内存里,然后直接让硬件开始执行目标代码:

编译+执行流程:源码 [字符流]
- 词法分析 -> 单词(token)流
- 语法分析 -> 语法树 / 抽象语法树
- 语义分析 -> 标注了属性的抽象语法树
- 代码生成 -> 目标代码
- 操作系统/硬件 -> 执行结果

与之前的分离流程相比,这里从输入源码到得到执行结果只有一步,从使用角度看似乎简单了一些,但同时也意味着每次重新执行同样的源码都必须重新经过从源码到生成目标代码之间的编译流程,冗余变多了。

然后我们可以进一步从后向前融合,不生成目标代码,而是让程序维持在一种中间形式上就开始解释执行。例如说:

编译+解释执行流程:源码 [字符流]
- 词法分析 -> 单词(token)流
- 语法分析 -> 语法树 / 抽象语法树
- 语义分析 -> 标注了属性的抽象语法树
- 不做类型检查的抽象语法树解释器 -> 执行结果

这里我们通过实现一个能在硬件上执行的抽象语法树解释器(AST interpreter,或者就叫tree interpreter)来实现源程序的执行。

要留意的是:由于在解释执行前做了语义分析(其中包括但不限于类型检查),我们可以相信输入到解释器的抽象语法树的类型是正确的,所以解释器里不必重复做类型检查。

其它可能在语义分析阶段做的处理诸如:

  • 变量的确定性赋值:变量必须在使用前先得到初始赋值;
  • 变量的确定性不重复赋值:不可变变量(例如Java的final变量)最多只能被赋值一次
  • 控制流的正确性校验:例如Java的continue语句只能用在循环体内、continue的跳转标签只能向更外围作用域而不能向更深的嵌套作用域跳转,等等;

在解释执行之前做好这些分析,就意味着在解释执行过程中完全不必关心这些检查,因而解释执行的效率就可以更高。

然后可以进一步去掉解释执行前的语义分析,变为:

编译+解释执行流程:源码 [字符流]
- 词法分析 -> 单词(token)流
- 语法分析 -> 语法树 / 抽象语法树
- 需要做类型检查的抽象语法树解释器 -> 执行结果

没有了解释执行前的语义分析,要维持语言的语义正确,就必须在解释执行过程中融入语义分析本来应该完成的动作。例如:

  • 在看到一个“赋值”动作时,必须检查赋值目标(“左手边”)
    • 在作用域内是否存在
    • 类型是否匹配
    • 是否是final变量并且已经得到过赋值
    • ⋯等等
  • 在运行时必须维护一个“循环嵌套栈”,在执行“continue语句”时必须检查当前是否在循环里,并且要动态查找continue的跳转目标
  • ⋯等等

这些解释执行时做的语义分析的结果都不会被保存下来,所以多次执行到同一块代码时就得重复做这些分析。

这样,同样是在抽象语法树上解释执行,这个解释器就比上一个版本的解释器要重复做更多处理,因而会更复杂以及更慢。

我们可以进一步把语法分析也融合到解释执行中,变为:

编译+解释执行流程:源码 [字符流]
- 词法分析 -> 单词(token)流
- 需要做语法分析+类型检查的单词流解释器 -> 执行结果

此时解释器就不是在抽象语法树,而是在单词流上做解释执行了。为了保证我们只接受符合语法规则的程序,我们还是得做语法分析——只是把它融合到了解释器里而已。

与上一个版本的解释器最大的不同时,这个版本在解释器不会保留语法树/抽象语法树,所以解释器会一边做语法分析一边解释执行,如果多次执行同一块代码就得重复做语法分析。

最后,我们可以把词法分析也融合到解释执行中:

解释执行流程:源码 [字符流]
- 需要做词法分析+语法分析+类型检查的字符流解释器 -> 执行结果

有了前面的讲解,相信这一步是怎么回事不必多说了。

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

我以前做的一套演讲稿里,第6到第9页就是讲这个话题的:

http://www.valleytalk.org/wp-content/uploads/2011/05/Java_Program_in_Action_20110727.pdf
 

使用解释器实现的编程语言实现里,通常:

  • 至少会在解释执行前做完语法分析,然后通过树解释器来实现解释执行;
  • 兼顾易于实现、跨平台、执行效率这几点,会选择使用字节码解释器实现解释执行。

在树解释器与字节码解释器中也各自有许多不同的变种,这里就不多展开说了。

在这两大类解释器中的取舍,请参考另一个回答:

为什么大多数解释器都将AST转化成字节码再用虚拟机执行,而不是直接解释AST? - RednaxelaFX 的回答
 

碰到这种话题我总是忍不住想放俩老传送门:

虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩[讨论]CPython能否用V8的方式优化性能

这篇关于【个人思考】 Java为什么解释执行时不直接解释源码?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1140539

相关文章

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Java使用Javassist动态生成HelloWorld类

《Java使用Javassist动态生成HelloWorld类》Javassist是一个非常强大的字节码操作和定义库,它允许开发者在运行时创建新的类或者修改现有的类,本文将简单介绍如何使用Javass... 目录1. Javassist简介2. 环境准备3. 动态生成HelloWorld类3.1 创建CtC

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

SpringBoot实现不同接口指定上传文件大小的具体步骤

《SpringBoot实现不同接口指定上传文件大小的具体步骤》:本文主要介绍在SpringBoot中通过自定义注解、AOP拦截和配置文件实现不同接口上传文件大小限制的方法,强调需设置全局阈值远大于... 目录一  springboot实现不同接口指定文件大小1.1 思路说明1.2 工程启动说明二 具体实施2

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志