什么是虚拟机,为什么动态语言需要它?

27

所以,例如Python和Java有一个虚拟机,C和Haskell没有。(如果我错了,请纠正我)

考虑一下这条线两侧的语言,我找不到原因。Java在很多方面都是静态的,而Haskell提供了许多动态特性。


11
Haskell 是静态类型语言。你想到哪些“动态特性”? - Jon Skeet
“Haskell提供了很多动态特性”是什么意思?它可能是世界上最强类型的语言。 - finnw
1
@Jon Skeet, @finnw: “泛型”被视为“动态特性”,因为常见的静态类型语言(如Java和C ++)对泛型的支持较差(语法笨拙,错误信息不够明确等),与常见的动态语言(如Python和Ruby)相比,后者对泛型有很好的支持。 - yairchu
3
@yairchu:我想我们对于“支持范型”的理解肯定是非常不同的——在仅支持动态语言的情况下,我的泛型概念甚至都没有意义。我认为泛型的“动态”特性与Python和Ruby的“动态”特性完全不同。 - Jon Skeet
2
@yairchu:那么Python和Ruby在哪些方面具有“良好的泛型支持”?它们不允许您以我理解的泛型方式表达泛型...从维基百科关于通用编程的文章中可以看到:“动态类型(例如Objective-C中的特性)和协议的谨慎使用规避了使用通用编程技术的需要,因为存在一般类型来包含任何对象。” - Jon Skeet
显示剩余2条评论
8个回答

29

这与静态 vs 动态无关。

相反,它是关于独立于底层硬件平台的 ("一次构建,到处运行" - 理论上...)

事实上,这也与语言无关。一个人可以编写生成 JVM 字节码的 C 编译器。一个人可以编写生成 x86 机器代码的 Java 编译器。


3
一个现实世界的例子:C#编译器通常会为CLR生成字节码,而MonoTouch编译器则会生成本地的iOS代码。 - R. Martinho Fernandes
14
有人“可能会”吗?有人已经这样做了。将C转换为Java字节码:NestedVM;将Java转换为机器码:gcj - ephemient
4
更进一步的例子:clang C编译器为LLVM虚拟机生成字节码。注意,LLVM不是像Java或.NET程序员所想的完整功能的虚拟机,因为它不提供操作系统环境抽象,只提供CPU抽象。 - Steve Jessop
3
@SpoonBender: 你不需要使用虚拟机来防止缓冲区溢出。这可以在语言层面完成,编译器可以根据需要静默生成运行时检查代码。比如 Ada 语言。但我同意,虚拟机有沙盒机制的内在优势。 - Oliver Charlesworth
14
"一次编写,跨平台运行"并不需要使用虚拟机。Haskell代码不需要重写即可在不同的平台上运行。对于ANSI C代码也是如此。你只需要虚拟机来实现"一次编译,跨平台运行"。 - sepp2k
显示剩余5条评论

23

在我们暂且不考虑虚拟机(我们稍后会回到这个问题上,我保证),让我们先看一下一个重要的事实:

C语言没有垃圾回收。

为了提供垃圾回收的功能,语言必须有某种"运行时"/运行环境/东西来执行它。

这就是为什么Python、Java和Haskell需要一个"运行时",而C不需要,可以直接编译成本地代码。

请注意,Psyco是一个将Python代码编译为机器码的优化器,但其中许多机器码都包含对C-Python运行时函数的调用,例如PyImport_AddModulePyImport_GetModuleDict等。

Haskell/GHC和psyco编译的Python类似。整数(Int)被添加为简单的机器指令,但分配对象等更复杂的内容会调用运行时。

还有什么?

C语言没有“异常”

如果我们要在C中添加异常,我们生成的机器码需要对每个函数和每个函数调用做一些处理。

如果我们再添加“闭包”,就会增加更多的处理。

现在,我们可以让每个函数调用子程序来执行必要的处理,而不是在每个函数中重复这些样板机器码。例如像PyErr_Occurred这样的函数。

因此,基本上每个原始源代码行都映射到一些函数调用和一个较小的唯一部分。

但只要我们对每个原始源代码行进行了如此多的处理,为什么还要使用机器码呢?

这里有一个想法(顺便说一下,让我们把这个想法称为“虚拟机”)。

让我们来表示您的Python代码,例如:

def has_no_letters(text):
  return text.upper() == text.lower()

作为一个内存数据结构,例如:

{ 'func_name': 'has_no_letters',
  'num_args': 1,
  'kwargs': [],
  'codez': [
    ('get_attr', 'tmp_a', 'arg_0', 'upper'),  # tmp_a = arg_0.upper
    ('func_call', 'tmp_b', 'tmp_a', []),  # tmp_b = tmp_a() # tmp_b = arg_0.upper()
    ('get_attr', 'tmp_c', 'arg_0', 'lower'),
    ('func_call', 'tmp_d', 'tmp_c', []),
    ('get_global', 'tmp_e', '=='),
    ('func_call', 'tmp_f', 'tmp_e', ['tmp_b', 'tmp_d']),
    ('return', 'tmp_f'),
  ]
}

现在,让我们编写一个解释器来执行这个内存数据结构。

让我们讨论一下它相对于直接从文本解释器的好处,然后再讨论与编译为机器代码的好处。

VM相对于直接从文本解释器的好处

  • VM系统在执行代码之前会给出所有的语法错误。
  • 在评估循环时,VM系统不会每次运行都解析源代码。
    • 使VM比直接从文本解释器更快。
    • 因此,直接解释器在长变量名上运行较慢,在短变量名上运行较快。这鼓励人们编写糟糕的数学家风格的代码,如wt(f, d(o, e), s) <= th(i, s) + cr(a, p * d + o)

VM相对于编译为机器代码的好处

  • 描述程序的内存数据结构,或者“VM代码”,可能会比针对每个原始代码行重复执行相同操作的样板代码产生更小的体积。这将使VM系统运行更快,因为需要从内存中获取的“指令”数量更少。
  • 创建VM比创建编译器到机器代码要简单得多。您现在可能甚至不需要了解任何汇编/机器代码。

4

VM(虚拟机)实际上是一种语言设计者避免编写语言实现复杂性的工具。

基本上,它是一个虚拟计算机规范,规定了该计算机的每个部件如何相互交互。您可以在此规范中编写一些假设,这些假设可以由实际语言使用或不使用。

在此规范中,通常定义处理器/处理器的工作方式、内存的工作方式、可行的读/写障碍等以及与其交互的较简单的汇编语言。

最终语言通常是从您编写的文本文件转换(编译)为针对该机器编写的表示形式。

这有一些优点:

  • 将语言与特定的硬件体系结构分离
  • 通常允许您控制发生的情况
  • 不同的人可以将其移植到不同的体系结构
  • 您可以获得更多信息以优化代码
  • 等等。

还有很酷的因素:看吧,我制作了一个虚拟机 :)


4
一个虚拟机基本上是一个解释器,它解释一种更接近于机器码的语言。当真正的机器解释真正的机器码时,虚拟机则解释一种虚构的机器码。有些虚拟机会解释实际计算机的机器码,这些被称为仿真器。
对于类似汇编语言的简单语言来说,编写解释器比编写完整的高级语言要容易得多。此外,许多高级代码结构通常只是一些基本原理的语法糖。因此,编写一个编译器,将所有这些复杂的概念转换为简单的虚拟机语言,这样我们就不必编写复杂的解释器,而可以使用简单的虚拟机。然后,您就有更多时间优化虚拟机。
这基本上是大多数现代语言(不编译成真正的机器码)的实现方式。
解释器(虚拟机)和编译器可以是分开的程序(如Java和javac),也可以是一个程序(如Ruby或Python)。

3

虚拟机的维基百科词条中可以得知:

"虚拟机(VM)是一种计算机软件实现,可以像物理机器一样执行程序。"

虚拟机最大的优点在于理论上的代码可移植性 - "编写一次,到处运行"

最著名的虚拟机例子可能是JVM,最初设计用于运行Java代码,但现在也越来越多地用于Clojure和Scala等语言。

动态语言没有特定的要求需要使用虚拟机。但是,它们确实需要一个解释器,该解释器可以建立在虚拟机上。


3
为什么他们需要翻译? - AProgrammer

2

没有“必要”,这些语言都提供编译器,直接发出机器代码来实现其语言在给定架构中的语义。

虚拟机的想法是为了抽象掉所有不同硬件和软件制造商之间的架构差异,以便开发人员有一个统一的机器可以编写。


2

Java和Python可以以维护平台无关性的方式进行编译。即使对于C#也是如此。优点在于虚拟机能够将这些大多数强类型字节码转换为非常好的平台特定代码,并具有相对较低的开销。由于Java旨在“一次构建 - 任何地方运行”,因此创建了JVM。


2
想象一下你创建了一种编程语言:你理清了语言的语义并开发了一个漂亮的语法。然而,仅有文本表示是不够的:每次执行程序时都需要解析文本是低效的,因此自然而然地添加了内存中的二进制表示。再加上自定义内存管理器,你基本上就拥有了一个虚拟机。
现在,为了额外得分,可以开发一个字节码格式来序列化内存中的表示和运行时加载器,或者如果想要使用脚本语言,则可以添加一个 eval() 函数。
最后,添加一个 JIT 编译器。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接