【个人思考】 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

相关文章

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

springboot自定义注解RateLimiter限流注解技术文档详解

《springboot自定义注解RateLimiter限流注解技术文档详解》文章介绍了限流技术的概念、作用及实现方式,通过SpringAOP拦截方法、缓存存储计数器,结合注解、枚举、异常类等核心组件,... 目录什么是限流系统架构核心组件详解1. 限流注解 (@RateLimiter)2. 限流类型枚举 (

Java Thread中join方法使用举例详解

《JavaThread中join方法使用举例详解》JavaThread中join()方法主要是让调用改方法的thread完成run方法里面的东西后,在执行join()方法后面的代码,这篇文章主要介绍... 目录前言1.join()方法的定义和作用2.join()方法的三个重载版本3.join()方法的工作原

Spring AI使用tool Calling和MCP的示例详解

《SpringAI使用toolCalling和MCP的示例详解》SpringAI1.0.0.M6引入ToolCalling与MCP协议,提升AI与工具交互的扩展性与标准化,支持信息检索、行动执行等... 目录深入探索 Spring AI聊天接口示例Function CallingMCPSTDIOSSE结束语

Java获取当前时间String类型和Date类型方式

《Java获取当前时间String类型和Date类型方式》:本文主要介绍Java获取当前时间String类型和Date类型方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录Java获取当前时间String和Date类型String类型和Date类型输出结果总结Java获取

Spring Boot Actuator应用监控与管理的详细步骤

《SpringBootActuator应用监控与管理的详细步骤》SpringBootActuator是SpringBoot的监控工具,提供健康检查、性能指标、日志管理等核心功能,支持自定义和扩展端... 目录一、 Spring Boot Actuator 概述二、 集成 Spring Boot Actuat

OpenCV在Java中的完整集成指南分享

《OpenCV在Java中的完整集成指南分享》本文详解了在Java中集成OpenCV的方法,涵盖jar包导入、dll配置、JNI路径设置及跨平台兼容性处理,提供了图像处理、特征检测、实时视频分析等应用... 目录1. OpenCV简介与应用领域1.1 OpenCV的诞生与发展1.2 OpenCV的应用领域2

在Java中使用OpenCV实践

《在Java中使用OpenCV实践》用户分享了在Java项目中集成OpenCV4.10.0的实践经验,涵盖库简介、Windows安装、依赖配置及灰度图测试,强调其在图像处理领域的多功能性,并计划后续探... 目录前言一 、OpenCV1.简介2.下载与安装3.目录说明二、在Java项目中使用三 、测试1.测

Spring Bean初始化及@PostConstruc执行顺序示例详解

《SpringBean初始化及@PostConstruc执行顺序示例详解》本文给大家介绍SpringBean初始化及@PostConstruc执行顺序,本文通过实例代码给大家介绍的非常详细,对大家的... 目录1. Bean初始化执行顺序2. 成员变量初始化顺序2.1 普通Java类(非Spring环境)(

Spring Boot 中的默认异常处理机制及执行流程

《SpringBoot中的默认异常处理机制及执行流程》SpringBoot内置BasicErrorController,自动处理异常并生成HTML/JSON响应,支持自定义错误路径、配置及扩展,如... 目录Spring Boot 异常处理机制详解默认错误页面功能自动异常转换机制错误属性配置选项默认错误处理