抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

一、解释型、编译型、混合型语言

要回答上述问题,我们要从最基础的讲起。

我们知道,CPU 只认识机器指令(也叫做 CPU 指令、机器码),用像 Python、C++、Java 这样的高级语言编写的代码,需要编译(编译的意思实际上就是“翻译”)为机器指令之后,才能被 CPU 执行。而不同类型的语言,其编译过程也是不同的,据此我们将其分为三类。

1 )编译型语言

对于类似 C++ 这样的编译型语言,代码会事先被编译成机器指令(可执行文件),然后再一股脑儿交给 CPU 来执行。在执行时,CPU 面对是已经编译好的机器指令,直接逐条执行即可,执行效率比较高。但因为每种类型的 CPU(比如 Intel、ARM 等)支持的 CPU 指令集不同,并且程序还有可能调用操作系统提供的 API,所以,编译之后的可执行文件只能在特定的操作系统和机器上执行,换一种操作系统或机器,编译之后的可执行文件就无法执行了。

CPU 架构 是 CPU 厂商给属于同一系列的 CPU 产品定的一个规范,主要目的是为了区分不同类型 CPU 的重要标示。目前市面上的 CPU 分类主要分有两大阵营,一个是 intel、AMD 为首的复杂指令集 CPU,另一个是以 IBM、ARM 为首的精简指令集 CPU。两个不同品牌的 CPU,其产品的架构也不相同,例如,Intel、AMD 的 CPU 是 X86 架构的,而 IBM 公司的 CPU 是 PowerPC 架构,ARM 公司是 ARM 架构。

2 )解释型语言

对于类似 Python 这样的解释型语言,代码并不会被事先编译成机器指令,而是在执行的过程中,由 Python 虚拟机(也叫做解释器)逐条取出程序中的代码,编译成机器指令,交由 CPU 执行,完成之后,再取出下一条代码,重复上述的编译、执行过程。这种 一边编译一边执行的过程,叫做解释执行。

解释型语言相对于编译型语言,执行速度会慢一些。因为程序是在执行的过程中一边编译一边执行的,所以,程序整体的执行时间包含了程序编译的时间。不过,使用解释性语言编写的代码,可移植性更好 。程序在执行的过程中, 虚拟机可以根据当前所在机器的 CPU 类型和操作系统类型,翻译成不同的 CPU 指令。这样,同一份代码就可以运行在不同类型的机器和不同类型的操作系统上。这就是常听到的“一次编写,多处运行”。

3)混合型语言

Java 语言比较特殊,它属于混合型语言,既包含 编译执行也包含解释执行

Java 编译器会先将代码(.java 文件)编译成字节码(.class 文件 )而非机器码,字节码算是 Java 代码的一种中间状态,其跟平台无关,但又可以快速地被翻译成机器码。编译之后的字节码在执行时,仍然是解释执行的,也就是逐行读出字节码,然后翻译成机器码,再交给 CPU 执行。只不过, 从字节码到机器码的翻译过程,比从高级语言到机器码的翻译过程,耗时要少。这样既保证了 Java 代码的可移植性(同一份代码可以运行在不同的 CPU 和操作系统上),又避免了解释执行效率低的问题。

实际上,在解释执行时,也存在编译执行。Java 虚拟机会将热点字节码(反复多次执行的代码,类似缓存中的热点数据),编译成机器码缓存起来,以供反复执行,这样就避免了热点字节码反复编译,进一步节省了解释执行的时间。这就是著名的 JIT 编译(Just In Time Compile,即时编译),这部分内容会在专栏的第三部分中详细讲解,这里就暂不展开。

二、CPU、操作系统、虚拟机

上面反复提到了 CPU、操作系统、虚拟机,现在,我们就来看下,它们在程序的执行过程中,扮演了什么角色。CPU 的工作非常明确,用来执行编译好的机器指令,我们重点看下操作系统和虚拟机。

1)操作系统在程序执行中的作用

早期的计算机还没有高级语言和操作系统,程序员用机器指令编写的代码,通过纸带打卡方式记录下来,传输给计算机(可以理解为 CPU)直接执行。

随着硬件资源越来越丰富,计算机中开始集成各种硬件设备,比如内存、硬盘、各种输入输出(键盘、鼠标、显示器等),并且,人们希望多个程序能在计算机中并发执行(比如听歌的同时还能打字),于是,操作系统就诞生了。

操作系统用来管理硬件资源和调度程序的执行。打个比方,CPU 等硬件就好比车间中的机器,工人就像操作系统,一个个程序就像一个个待执行的任务。工人(操作系统)调度机器(CPU 等硬件)来执行各个任务(程序)。

除此之外,操作系统还担当了类库的作用 。对于通用的功能代码,比如读写硬盘等,没必要在每个程序中都从零编写一遍。 操作系统将这些通用的功能代码,封装成 API(专业名称叫做系统调用),供我们在编写应用程序时直接调用。也就是说,在应用程序的执行过程中,CPU 可能会跳转去执行操作系统中的某段代码。当然,这段代码肯定是已经编译好的机器指令。

2)虚拟机在程序执行中的作用

我们先下来对比一下,C++ 代码、Python 代码、Java 代码编译和执行的命令。如下所示。

1
2
3
4
5
6
7
8
9
10
// C++  
$ g++ helloword.cpp -o helloworld
$ ./helloword

// Python
$ python helloworld.py

// Java
$ javac HelloWorld.java
$ java HelloWorld

仔细观察上述命令行的区别,我们可以看出,C++ 编译之后的代码直接就可以执行,而 Python 和 Java 代码的执行,需要依赖其他程序,也就是虚拟机,表现在命令行中就是前面有 python、java 字样。

使用解释型和混合型语言编写的代码,执行过程都需要虚拟机的参与。实际上,虚拟机本身也可以看做一个程序,而且它已经是 CPU 可以执行的机器指令了。程序员编写的代码相当于嵌套在虚拟机程序中一个插件(或者功能模块),只不过,它跟虚拟机本身的代码有点不同,无法直接交给 CPU 执行。虚拟机将字节码翻译成 CPU 指令,放到固定的内存位置,再通过修改 CPU 寄存器中存储的地址的方式,引导 CPU 执行这块内存中存储的 CPU 指令(关于这一部分,我们待会会详细讲解)。

如果你是一名经验丰富的 Java 工程师,不知道你有没有感觉到,虚拟机的这套解释执行的机制,跟 Java 的反射语法异曲同工,反射是在代码的执行过程中,将字符串(类名、方法名等)转化成代码之后再被执行。而虚拟机的解释执行是将字节码(也可以看做是字符串)转化成 CPU 指令再执行。

三、CPU 指令、汇编语言、字节码

前面反复提到 CPU 指令、字节码,估计你对此会很好奇,它们到底长什么样子。现在,我们就来具体看下。不过,提到 CPU 指令,免不了要讲一下汇编语言,所以,接下来,我们也会一并讲一下汇编语言。站在操作系统和 CPU 的角度,Java 程序编译之后的字节码跟虚拟机合并在一起,才算是一个完整的程序,才相当于 C++ 编译之后的可执行文件。CPU 在执行程序员编写的代码的同时,也在执行虚拟机代码。而且是先执行虚拟机代码,然后才是引导执行程序员编写的代码。

1 )CPU 指令

前面提到,我们经常说的 CPU 指令、机器码、机器指令,实际上都是一个东西,就是 CPU 可以完成的操作。一条 CPU 指令包含的信息主要有:操作码、地址、数据三种,指明所要执行的操作、数据来源、操作结果去向。

cpu 可以完成的所有的操作,叫做指令集。常见的指令集有 X86、X86-64、ARM、MIPS 等。不同的 CPU 支持的指令集可能不同(Intel CPU 支持的 X86 指令集,ARM CPU 支持指令集 ARM)。当然,不同的 CPU 支持的指令集也可以相同(比如 Intel 和 AMD 的 CPU 都支持 X86 和 X86-64 指令集)。同一个 CPU 也可以支持多种指令集(Intel CPU 支持的指令集有 X86、X86-64 以及其他扩展指令集)。CPU 支持的指令集一般都非常庞大,例如 Intel CPU 支持 2000 多条指令,可以完成诸多不同类型的操作。

2)汇编语言

前面提到,在计算机发展的早期,程序员直接使用机器码来编写程序。但是,因为机器码是二进制码,所以,编写起来复杂、可读性也不好。为了解决这个问题,汇编语言就被发明出来了。汇编语言由一组汇编指令构成 ,汇编指令跟 CPU 指令一一对应, 但汇编指令采用字符串而非二进制数来表示指令,所以,可读性好很多。实际上,CPU 指令和汇编指令之间的关系,就类似 IP 地址和域名之间的关系,IP 地址和域名一一对应,域名的可读性比 IP 地址好。

程序员使用汇编语言编写的代码,需要经过编译,翻译成机器码才能被 CPU 执行。这个编译过程有个特殊的名称,叫做“汇编”。C/C语言的编译过程,实际上就包含汇编这一步骤。编译器先将 C/C代码编译成汇编代码,然后再汇编成机器码。

我们拿一段 C 语言代码来举例,如下所示,看看编译之后的汇编代码长什么样子。

1
2
3
4
5
6
7
8
// hello.c
#include <studio.h>
int main(){
int a = 1;
int b = 2;
int c = a + b;
return c;
}

上述代码编译为汇编代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ gcc -S hello.c  
$ cat hello.s
_main:                                  ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movl $0, -4(%rbp)
movl $1, -8(%rbp)
movl $2, -12(%rbp)
movl -8(%rbp), %eax
addl -12(%rbp), %eax
movl %eax, -16(%rbp)
movl -16(%rbp), %eax
popq %rbp
retq

有同学可能有疑问,这个汇编代码跟他见过的不大一样,实际上,汇编指令跟 CPU 指令一一对应,相同指令集对应的汇编指令集都是一样的(比如上面的代码是 Intel 的 CPU 支持的 X86 指令集对应的汇编代码),只不过语法格式有可能不同。汇编语言主要有两种语法格式,一种是 Intel 风格的,一种是 AT&T 风格的。上述汇编代码是 AT&T 风格,对应的 Intel 风格如下所示。对比上下两段汇编代码,唯一的区别就是格式不同而已。注意下面这段代码添加了部分注释,汇编中的注释以分号开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ gcc -S -masm=intel hello.c  
$ cat hello.s
_main:                                  ## @main
.cfi_startproc
## %bb.0:
push rbp
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp
.cfi_def_cfa_register rbp
mov dword ptr [rbp - 4], 0  ; 安全 word,没有实际意义
mov dword ptr [rbp - 8], 1  ;int a = 1; 存储在栈中 rbp-8 的位置
mov dword ptr [rbp - 12], 2 ;int b = 2; 存储在栈中 rpb-12 的位置
mov eax, dword ptr [rbp - 8] ;a 的值累加在寄存器 eax 上
add eax, dword ptr [rbp - 12] ;b 的值累加在寄存器 eax 上
mov dword ptr [rbp - 16], eax ;int c = a+b
mov eax, dword ptr [rbp - 16] ;return c。返回值通过 eax 返回给上层。
       ; 所以,把 c 值赋值给 eax。因为上一句指令结束之后,
       ; 有可能还有其他运算还会用到 eax(本程序比较简单,没有其他运算了),
       ; 所以 eax 中的值有可能会被改变,所以程序返回时重新将 c 值重新赋值给 eax。
pop rbp
ret

对于没学习过汇编语言的同学,完全理解上述汇编代码可能比较困难。不过,这里,我们只需要简单了解汇编代码长什么样子,有个直观的认识即可。关于以上汇编代码的具体解释,我们在下一节会讲到。

3)字节码

我们常说,Java 语言是跨平台的,“write once, run anywhere”(一次编写,多处运行)。程序员编写的代码,在不需要任何修改的情况下,就可以运行在不同的平台上(不同的操作系统和 CPU)。

有人认为,Java 语言之所以能跨平台,是字节码的功劳。因为字节码跟平台无关,我们在一个平台上编译得到的字节码,可以运行在其他平台上。

实际上,这样的观点是不确切的。毕竟没有字节码的解释型语言也可以跨平台。字节码诞生的目的是,克服解释型语言解释执行速度慢的缺点(字节码是介于高级语言和机器码之间的形态,比高级语言解释执行更快)。字节码跟平台无关,是为了让 Java 语言保留解释型语言跨平台的优点,而不是促使 Java 语言跨平台的最根本原因。这一点要搞清楚。

之所以 Java 语言能做到跨平台,最根本原因是有虚拟机的存在。Java 代码跟平台无关,字节码跟平台无关,在编译执行过程中,总要有一个环节跟平台有关,不然,跟平台有关的、最终可以被 CPU 执行的机器码从何而来。俗话说的好,哪有什么岁月静好,只是有人帮你负重前行。跟平台有关的环节就是解释执行环节,而这个环节的主导者就是虚拟机。

虚拟机是已经编译好的可以被 CPU 执行的机器码,而机器码又是跟平台有关的,因此虚拟机必须跟平台有关。这也是为什么在不同的平台(操作系统和 CPU)上,我们需要下载不同的 Java 虚拟机的原因。

明确了字节码的作用之后,我们通过一个简单的例子,来看下字节码到底长什么样子?

1
2
3
4
5
6
7
public class HelloWorld {  
  public static void main (String[] args) {
    int a = 1;
    int b = 222222;
    int c = a + b;
  }
}

对于上面的代码,我们使用 javac 命令编译之后,会得到一个后缀为.class 的文件 HelloWorld.class,这个文件里保存的一堆二进制码就是字节码。为了方便查看,我们用 vi 打开转化成 16 进制格式之后,如下所示:

显然,这样的二进制文件是交给虚拟机解析的,人类阅读起来非常费劲。我们可以 使用 javap 工具,将 class 文件解析成适合人类阅读的格式,如下所示。这个过程被叫做反编译。为啥叫做反编译呢?我们可以跟“编译”对比着理解。编译是将贴近人阅读的内容,翻译成贴近机器阅读的内容的过程。对应地,反编译就是将贴近机器阅读的内容,翻译成贴近人阅读的内容的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$ javap -verbose HelloWorld   
Classfile /Users/wangzheng/Desktop/HelloWorld.class
  Last modified 2022-3-20; size 291 bytes
  MD5 checksum ea48b76348ad4f0c07a2bf96840e8e00
  Compiled from "HelloWorld.java"
public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Integer            222222
   #3 = Class              #14            // HelloWorld
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               HelloWorld.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               HelloWorld
  #15 = Utf8               java/lang/Object
{
  public HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1   // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0


  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: ldc           #2     // int 222222
         4: istore_2
         5: iload_1
         6: iload_2
         7: iadd
         8: istore_3
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 5
        line 6: 9
}
SourceFile: "HelloWorld.java"

不过,你有没有产生过这样的好奇,class 文件为什么叫字节码?跟字节(byte)有什么关系呢?实际上,class 文件里包含了很多类的信息,而其中 main()函数中的代码对应的字节码指令(字节码指令是二进制的,下面的是反编译之后的助记符)只有下面这几行:

1
2
3
4
5
6
7
8
9
0: iconst_1  // 把 int 型 1 入栈  
         1: istore_1  // 把栈顶 int 型数值存入第一个变量,也就是 a
         2: ldc #2    // 将常量池中的 #2 号常量 222222 入栈
         4: istore_2  // 把栈顶 int 型数值存入第二个变量,也就是 b
         5: iload_1   // 把第一个变量的值入栈
         6: iload_2   // 把第二个变量的值入栈
         7: iadd      // 将栈顶的两个 int 型数据相加,结果压入栈
         8: istore_3  // 将栈顶 int 型数值存入第三个变量,也就是 c
         9: return    // 程序返回

一条字节码指令有些包含操作码和操作数两部分,有些只包含操作码这一部分。因为操作码长度为一个字节,所以,这种指令格式被叫做字节码。从另一个角度,我们也可以得知,字节码的操作码类型不超过 256 个(2^8)。相对于 CPU 指令和汇编指令,字节码指令少很多。这是因为字节码指令相对于 CPU 指令来说,抽象程度更高。一条字节码指令完成的逻辑,比一条 CPU 指令完成的逻辑,更加复杂。

Java8 的字节码指令集 点击此处 查看

四、代码是如何被 Cpu 执行的

有了以上铺垫,我们最后讲解一下:“代码是如何被 CPU 执行的”。

不管是使用哪种类型的编程语言(编译型、解释型、混合型)编写的代码,不管经历什么样的编译、解释过程,最终交给 CPU 执行的都是机器码。

提到 CPU,就不得不提寄存器。

我们知道,内存的读写速度比起 CPU 指令的执行速度要慢很多。从内存中读取 32 位二进制数据所耗费的时间,相当于 CPU 执行上百条指令所耗费的时间。所以,CPU 在执行指令时,如果依赖内存来存储计算过程中的中间数据,那么,CPU 总是在等待读写内存操作的完成,势必非常会影响 CPU 的计算速度。为了解决这个问题,寄存器就被发明出来了。

寄存器读写速度非常快,能够跟 CPU 指令执行速度相匹配。所以,数据会先读取到寄存器中再参与计算。不过,你可能会说了,数据在计算前需要先从内存读取到寄存器,计算之后存储在寄存器中的结果也要写入到内存,寄存器的存在并没有避免掉内存的读写,使用寄存器是不是多此一举呢?实 际上,尽管最初数据来源于内存,最后计算结果也要写入内存,但中间的计算过程涉及到一些临时结果的存取,都可以在寄存器中完成,不用跟非常慢速的内存进行交互。顺便说一句,计算机为了提高 CPU 读写内存的速度,还引入了 L1、L2、L3 这三级缓存。

寄存器为了做到能让 CPU 高速访问,硬件设计比较特殊(高成本、高能耗),且与 CPU 距离很近(相对于内存来说,寄存器直接跟 CPU 集成在一起),这些也决定了寄存器的个数不会很多。不同的 CPU 包含的寄存器会有所不同。常见的寄存器有以下几类。

1)通用寄存器:AX,BX,CX,DX

通用寄存器区别于下面要讲到的特殊寄存器,它们一般用来存储普通数据。AX,BX,CX,DX 这四种通用寄存器的用途又有所区别,比如 AX 是累加器,这些细节我们就不展开讲解了。

2)指针寄存器:BP,SP,SI,DI,IP

BP(Base Pointer Register)和 SP(Stack Pointer Register)是用于存储栈空间地址的寄存器,SP 存储栈顶地址,BP 比较特殊,一般存储栈中一个栈帧的栈底地址。这一部分在下一节讲解函数的执行过程时再详细讲解。

SI(Source Index Register)源地址寄存器和 DI(Destination Index Register)目的地址寄存器,分别用来存储读取和写入数据的内存地址。

IP(Instruction Pointer Register)指令指针寄存器存储下一条将要执行的指令的内存地址(此处的描述不够准确,下文解释)。

3)段寄存器:CS,DS,SS

程序由一组指令和一堆数据组成。指令存储在某块内存中(这块内存被称为代码段),由 CPU 逐一读取执行。数据也存储在某块内存中(这块内存被称为数据段)。指令执行的过程中,会操作(读取或写入)这块内存中的数据。

CS(Code Segment Register)代码段地址寄存器存储了代码段的起始地址。上文中讲到,IP 寄存器中存储的是下一条将要执行的指令的内存地址,实际上,这样的说法是不准确的。CS 和 IP 两个寄存器中存储的内容如下计算,才能得到一个真正的物理内存地址。

1
物理内存地址 = 段地址(如 CS) * 16 + 偏移地址(如 IP)

我们拿 8086 CPU(早期 16 位的 X86 CPU)举例解释。8086 CPU 具有 20 位地址总线,支持 1MB 内存的寻址能力。对于 16 位的 IP 寄存器,只能存储 64K(2^16)个内存地址,一个字节占一个地址,那么只能支持 64KB 大小内存的寻址。为了扩大寻址能力,计算机使用段地址和偏移地址相结合的方式来确定一个物理内存地址。

对于存储下一条将要执行的指令的地址的寄存器,你或许还听到过 PC 寄存器(Program Counter Register)这种叫法。实际上,PC 寄存器是一个抽象概念,CS 寄存器和 IP 寄存器是具体寄存器的名称。我们可以简单理解为 PC 寄存器就是 CS 寄存器和 IP 寄存器的抽象统称。

DS(Data Segment Register)数据段地址寄存器存储了数据段的起始地址,同理,它跟 DI 或 SI 结合才能确定一个数据段中的内存地址。

SS(Stack Segment Register)栈寄存器存储的是栈的起始地址,同理,它跟 SP 结合才能确定栈顶的内存地址,跟 BP 结合才能确定栈中某个中间位置的内存地址。有些同学看到这里可能会有疑问,数据段是存储数据的,栈也是存储数据,这两者有什么联系呢?关于这个问题,我们在下一节文章中会讲到。

4)指令寄存器:IR

IR(Instruction Register)指令寄存器用来存放当前正在执行的指令。指令为一串二进制码,指令译码器需要从指令中解析出操作码和操作地址或操作数。所以,指令需要暂存在指令寄存器中等待译码处理。

5)标志寄存器

FR(Flag Register)标志寄存器,也叫做程序状态字寄存器(Program Status Word,PSW), 在这个寄存器中的每一个二进制位记录一类状态。比如 cmp 比较大小指令的运算结果会存储在 ZF 零标志位或 CF 进位标志位中。关于更多细节,我们不展开讲解。

以上寄存器都是用来辅助完成 CPU 的各种指令。注意,以上是 16 位的寄存器,32 位的寄存器名称在对应的 16 位寄存器名称前加 E(例如 EAX,EBP,ESP,EIP),64 位的寄存器名称在对应的 16 位寄存器名称前加 R(例如 RAX,RBP,RSP,RIP)。在下一节中,在讲解编程语言基本语法的底层实现原理时,我们还会提到更多寄存器的使用细节。

了解了 CPU 指令执行的重要部件寄存器之后,我们来看下 CPU 执行指令的流程。

对于编译型语言,操作系统会把编译好的机器码,加载到内存中的代码段,将代码中变量等数据放入内存中的数据段,并且设置好各个寄存器的初始值,如 DS、CS 等。IP 寄存器中存储代码段中第一条指令的内存地址相对于 CS 的偏移地址。

CPU 根据 PC 寄存器(CS 寄存器和 IP 寄存器的总称)存储的内存地址,从对应的内存单元中取出一条 CPU 指令,放到 IR 指令寄存器中,然后将 IP 寄存器中的地址 +4(也就是下一条指令在代码段中的偏移地址。内存中的每一个字节都对应一个地址。对于 32 位 CPU,一条指令长度为 4 字节,下一条指令地址 = 当前指令地址 +4。对于 64 位 CPU,一条指令长度是 8 字节,下一条指令地址 = 当前指令地址 +8)。一条指令执行完成之后,再通过 PC 寄存器中的地址,取下一条指令继续执行。循环往复,直到所有的指令都执行完成。

对于解释型或混合型语言,操作系统将虚拟机本身的机器码,加载到内存中的代码段,然后一条一条地被 CPU 执行。这部分被执行的指令对应的功能,就包括把程序员编写的程序解释成机器码这一功能。虚拟机把解释好的机器码会放到某段内存中,然后将 PC 寄存器的地址设置为这段内存的首地址,于是,CPU 就被虚拟机引导去执行程序员编写的代码了。

在本节中,我们把程序用到的内存,粗略地分为代码段和数据段,对于 Java 语言来说,其虚拟机对内存还做了更加细致的划分,这部分内容我们在专栏的 JVM 部分讲解。

五、课后思考题

1)在你熟悉的语言中,有哪些是解释型语言?哪些是编译型语言?哪些是混合型语言?

2)本节中提到,C/C代码会先编译成汇编代码,再汇编成机器码,才能被执行。按理来说,汇编代码跟机器码一一对应,为什么 C/C代码不直接编译成机器码呢?先编译成汇编代码不是多此一举吗?

# 未理解 : 寄存器的各种形式

评论区:

分享你的观点