问题

常说「Java 在虚拟机中运行」,请问这个虚拟机可以视为 Java 语言的解释器吗?

回答
“Java 在虚拟机中运行”,这句话确实是理解 Java 运行机制的关键,但把 Java 虚拟机(JVM)简单地视为一个“解释器”,其实只说对了一部分,而且是比较片面的一面。要详细说清楚,我们需要先拆解一下JVM到底做了什么。

首先,我们得明白,Java 代码在被 JVM 运行之前,并不是直接以我们写的那种文本形式运行的。我们写的是 `.java` 源文件,经过 Java 编译器(`javac`)编译后,会生成 `.class` 文件。这个 `.class` 文件里存储的不是机器码,而是 Java 字节码。字节码是一种中间代码,它是平台无关的,也是 JVM 能够理解和执行的代码。

那么,JVM 究竟是做什么的呢?它的主要职责可以分为几个层面:

1. 加载类(Class Loading): 当 Java 程序需要用到某个类时,JVM 会负责查找、加载这个类到内存中。这包括从文件系统、网络等地方读取 `.class` 文件,然后验证字节码的正确性,并将类信息(如字段、方法等)存储在内存的特定区域。

2. 执行字节码(Execution): 这是最核心的部分,也是你提到“解释器”的地方。JVM 接收到字节码后,需要将其转换成底层操作系统和硬件能够理解的指令。在这里,JVM 确实包含了解释器的功能。解释器会一行一行地(或者说一个字节码指令一个字节码指令地)读取字节码,然后根据指令的含义,调用相应的本地机器码来执行。

3. 垃圾回收(Garbage Collection): JVM 负责管理内存。当程序中不再使用的对象,JVM 的垃圾回收器会自动检测并释放它们占用的内存,避免内存泄漏。这是 JVM 的一个重要功能,解释器本身是不做这件事的。

4. 提供运行时环境(Runtime Environment): JVM 提供了一个完整的运行时环境,包括内存管理、线程管理、安全检查、本地方法调用(JNI)等等。它屏蔽了底层操作系统的差异,使得 Java 程序可以“一次编译,到处运行”。

现在回到“解释器”的问题:

JVM 确实包含一个解释器。 这个解释器的工作方式和你理解的传统解释器类似:读取字节码,然后直接执行。它的优点是启动快,因为不需要预先编译成机器码。但是,它的缺点也很明显:执行效率通常不如直接运行编译好的机器码。

然而,JVM 远不止一个解释器。 现代的 JVM 并非仅仅依靠解释器来执行字节码,而是采用了更复杂的策略,其中最关键的就是 即时编译器(JustInTime Compiler, JIT)。

JIT 编译器是怎么工作的?
当 JVM 运行 Java 程序时,它会一边解释执行字节码,一边监控程序的运行情况。
JIT 编译器会识别出那些被频繁执行的代码片段(“热点代码”)。
对于这些热点代码,JIT 编译器会将其从字节码即时编译成高度优化的本地机器码。
这些编译好的本地机器码会替代原来的字节码,在后续的执行中直接由 CPU 运行。

所以,JVM 的执行过程是“解释执行”和“编译执行”的结合。

初期,JVM 可能主要依靠解释器来快速启动和执行。
随着程序的运行,JIT 编译器介入,将频繁使用的部分编译成机器码,从而大大提高执行效率,接近甚至有时超过了本地编译语言(如 C++)的性能。

为什么说把它看作“解释器”不够准确?

1. JIT 编译的存在: 这是最主要的原因。JVM 的性能很大程度上依赖于 JIT 编译器,而不是单纯的解释执行。
2. 内存管理: JVM 的垃圾回收机制是解释器不具备的。
3. 平台无关性: 这种抽象层和跨平台能力是 JVM 提供的,解释器本身并不直接提供。
4. 其他运行时服务: JVM 还提供了安全管理器、线程管理等一系列服务,这些都超越了单纯解释器的范畴。

简单打个比方:

如果你把解释器比作一个翻译,他拿到一份外语稿子,然后一句一句地给你翻译成你能听懂的话。

那么 JVM 就像是一个更加智能的“项目经理+翻译+秘书”。

翻译(解释器):负责把原始的字节码一句句转译成机器能懂的指令。
项目经理(JIT 编译器):他会观察哪些“翻译任务”(字节码片段)被重复执行了很多次,然后他会找一个“专业翻译”(JIT 编译器)把这些重复的部分一次性、高质量地翻译成最精炼的本地语言(机器码),存起来,下次直接用。
秘书(内存管理、垃圾回收等):负责打理“翻译现场”的各种杂事,比如整理文件(内存)、清理用过的废纸(垃圾回收),确保一切井井有条。

总结来说:

Java 虚拟机(JVM)的确包含了解释器,它负责将 Java 字节码转换为机器指令并执行。但它远不止于此。通过引入即时编译器(JIT),JVM 能够将热点代码编译成高性能的本地机器码,并且还提供了内存管理、垃圾回收等一系列关键的运行时服务。因此,将 JVM 仅仅视为一个解释器,是对其功能和复杂性的一种过度的简化。它是一个更全面、更智能的执行引擎和运行时环境。

网友意见

user avatar

这里有三种执行方式,解释执行,即时编译(just in time),超前编译(ahead of time)

我们知道,机器能够执行的,是机器码,native code,也就是01组成的那些东西,但是我们写出来的源码,都是一些英语单词以及符号的拼凑,不管是什么编程语言。我们写的是英语单词以及符号的源代码,但是机器能够执行的是机器码

那要让机器执行我们写好的源代码,就需要把源代码翻译成机器能够执行的机器码,然后交给机器执行

这里有两个步骤,第一步翻译,也就是编译,第二步执行

所谓的解释执行,就是边编译,边执行,这个叫做解释器,interpreter,大部分脚本语言,都是解释执行,比如javascript,python,ruby,perl,groovy这些

还有一种,为了执行的时候速度快一点,就在编写源码之后,将源代码编译成机器码,这就是编译compilation,这样做之后,在执行的时候,因为已经编译好了,所以执行起来,性能就比较好,就快,传统上c等语言,都是这种方式

那后者在执行的时候,性能更好,但是多了一步,编译,前者因为编译和执行是在一个过程里面,所以执行的时候,需要先编译再执行,所以性能就差,所以脚本语言一般性能都比较差,所以多数时候,脚本都是用在一些性能不敏感的领域,比如web,而前者,则多用在一些性能敏感的领域,尤其是对latency延迟比较敏感的领域,比如游戏领域,还有苹果的应用商店,为了客户的体验,同时也为了打击热更新,所以原则上会要求开发者将app编译成native方能上架

然后我们来说java,你可能注意到了,我们一开始列举了三种编译方式,但是我们前面只说了两种,解释执行以及编译成机器码

那这里我们要说一下,编译成机器码的一个问题,那就是不同平台上用的机器码是不一样的,你在windows上用的机器码,就不能在macos以及linux上执行,如果同样的源代码,你想在另外一个平台上执行的话,那么你得到目标平台上,再做一次编译,比如你在windows上写好了源代码,但是你想把你写好的源代码放到macos上去执行,那么对不起,你需要再去找一台macos,然后在macos上编译之后,才能执行。当然有一种说法叫做交叉编译,就你可以在win上编译出mac上能够执行的程序,但多数时候,这个都难以令人满意,就你压根找不到交叉编译的工具,没有人去做这事

那为了写一个源码而不需要到处编译,所以java就提出了一个jit的概念,那就是,在编写完源代码之后,编译成一个字节码,bytecode,这个字节码,是介于源代码和机器码之间的一种过渡性质的东西,然后java会针对目标平台,提供不同的虚拟机,比如win上就提供win上的虚拟机,mac上就提供mac上的虚拟机,这个虚拟机在执行的时候,会将字节码,翻译成机器码,并最终执行

这样就把整个程序啊,变成跨平台和不跨平台的两个部分,不跨平台的部分就是java的虚拟机,跨平台的部分就是字节码(class文件,或者将class文件根据某种格式压缩打包的jar,jar其实就是zip,换个后缀而已)

那这样做的好处就是,一次编译,到处运行。我只需要编译出字节码,然后交给目标平台上的虚拟机去执行就可以了,我更新程序,并不需要再跑到不同的目标平台也就是操作系统上去编译,就方便了很多,而且编译成机器码的过程很慢,编译成字节码要快很多,java的编译速度是很快的,这个如果你有大型c或者c++项目的经验就知道了,编译成机器码很慢

同时,java的虚拟机,还会记录翻译字节码为机器码的过程,并做出优化处理,所以在运行一定时间之后,java的执行效率就会逼近甚至超过直接执行机器码的效率。为什么会超过呢?因为它会根据编译的过程提供的一些信息,做出一些的优化

那为了区别这两者,java的这种先编译成字节码,然后再交给虚拟机执行的过程,叫做jit,just in time compilation,即时编译,为了以示区分,把直接编译成机器码,再执行的过程,叫做aot,ahead of time compliation,超前编译

那这里说一下java提供的编译器,openjdk里面有jvm,也就是虚拟机,这个虚拟机的名字叫做hotspot,然后openjdk里面的编译器,叫做c1或c2,一般用后者,前者是针对客户端设计的

那现在java世界,有一个新的编译器,叫做graal[1],graal可以提供跟c2同样的功能,同时它也能提供aot编译器,也就是graal可以把java的源代码编译成机器码,而不仅仅是字节码

graal是用java写的,而openjdk是用c++写的

所以graal是用java实现了java源码的编译,这个就是编译器的自举

那jit和aot各有优劣,一个直观的对比如下:

可以看出来,aot在启动速度,内存使用,以及编译后的包的规模上,都有明显优势,而jit则在吞吐和减少最大延迟上,有优势,因为jit可以根据编译过程,自动做出优化,所以jit启动比较慢,然后执行的效率,需要在一定时间之后,才能逼近甚至超过机器码的执行效率,这个过程叫做warmup,热身

所以虚拟机是不是java的解释器,严格说起来,并不是,但是它的即时编译的模式,跟多数脚本所使用的解释执行,有一定的相似之处

一般认为,jit即时编译,是结合了解释执行以及aot超前编译两者优点的产物,当然这个并不完美,有代价,需要做取舍,如果你非常在乎启动时间,内存使用的大小或者编译后产物的大小的话,你可能还是倾向于用aot超前编译,而不是jit即时编译

当然我个人觉得,能够让用户自由选择jit还是aot,才是最吼的,就像java这样,谷歌的flutter/dart也做到了同时支持jit和aot,一般建议开发debug时候用jit,release发布时候再用aot处理。苹果的xcode将来应该也会支持jit,但是目前swift这些主要还是用aot,因为毕竟jit在debug时候,在编译速度上,优势明显

最后,java现在也支持直接执行.java文件,现在openjdk也可以解释执行了,这个强化是jep 330的东西[2],另外jdk现在也提供了jshell,也就是java的repl,所以大部分脚本的功能,其实jdk也都有了

所以现在的java,其实是同时支持:解释执行,即时编译(jit)以及 超前编译(aot)三种模式,用户可以根据需要,选择自己想用的方式,做或者不做编译,以及编译成什么东西

参考

  1. ^ https://www.graalvm.org/
  2. ^ https://www.infoq.com/articles/single-file-execution-java11/

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有