1、代码是如何被执行的

内容来自王争 Java 编程之美

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

1.1、编译型语言

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

1.2、解释型语言

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

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

1.3、混合型语言

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

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

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

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

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

2.1、操作系统在程序执行中的作用

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

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

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

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

2.2、虚拟机在程序执行中的作用

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

$ g++ helloword.cpp -o helloworld
$ ./helloword

$ python helloworld.py

$ javac HelloWorld.java
$ java HelloWorld

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

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

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

站在操作系统和 CPU 的角度,Java 程序编译之后的字节码跟虚拟机合并在一起,才算是一个完整的程序,才相当于 C++ 编译之后的可执行文件
CPU 在执行程序员编写的代码的同时,也在执行虚拟机代码,而且是先执行虚拟机代码,然后才是引导执行程序员编写的代码

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

前面反复提到 CPU 指令、字节码,估计你对此会很好奇,它们到底长什么样子,现在,我们就来具体看下
不过,提到 CPU 指令,免不了要讲一下汇编语言,所以,接下来,我们也会一并讲一下汇编语言

3.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 多条指令,可以完成诸多不同类型的操作

3.2、汇编语言

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

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

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

Compiler Explorer

image

#include <stdio.h>

int main() {
    int a = 1;
    int b = 2;
    int c = a + b;
    return c;
}
$ 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
$ 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.3、字节码

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

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

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

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

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

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 进制格式之后,如下所示

cafe babe 0000 0034 0010 0a00 0400 0d03
0003 640e 0700 0e07 000f 0100 063c 696e
6974 3e01 0003 2829 5601 0004 436f 6465
0100 0f4c 696e 654e 756d 6265 7254 6162
6c65 0100 046d 6169 6e01 0016 285b 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
2956 0180 0a53 6f75 7263 6546 696c 6501
000f 4865 6c6c 6f57 6f72 6c64 2e6a 6176
610c 0005 0006 0100 0a48 656c 6c6f 576f
726c 6401 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0021 0003 0004 0006 0000
0002 0001 0005 0006 0001 0007 0000 001d
0001 0001 0000 0005 2ab7 0001 b100 0000
0100 6800 0000 0600 0100 0000 0100 0900
0900 0a00 0100 0700 0060 2e00 0200 0400
0000 0a04 3c12 023d 1b1c 603e b100 0000
0100 0800 0000 1200 4000 0000 3000 0200

这堆二进制码看似天书,实则有固定格式
下图是 class 文件的大体格式,例如,前 4 个字节为魔数(Magic Number),为固定值:0xCAFEBABE,用以表示此文件为字节码文件
紧跟着的 4 个字节表示编译器的 JDK 版本(major version 和 minor version)
当然,还有很多细节无法展示在下图中,在本节中,我们不展开介绍,在后面的内容中,慢慢学习
image
显然,这样的二进制文件是交给虚拟机解析的,人类阅读起来非常费劲
我们可以使用 javap 工具,将 class 文件解析成适合人类阅读的格式,如下所示
这个过程被叫做反编译,为啥叫做反编译呢?我们可以跟 "编译" 对比着理解
编译是将贴近人阅读的内容,翻译成贴近机器阅读的内容的过程;对应地,反编译就是将贴近机器阅读的内容,翻译成贴近人阅读的内容的过程

$ 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() 函数中的代码对应的字节码指令(字节码指令是二进制的,下面的是反编译之后的助记符)只有下面这几行

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 指令完成的逻辑,更加复杂

Java 8 的字节码指令集

顺道提一句,在上述字节码中,a、b 两个变量要先入栈中再计算,计算的结果也会放到栈中
为什么 Java 将 c = a + b 代码翻译成基于栈来运算的几条字节码呢?关于这一点,我们今天不展开讲解,在本专栏的 JVM 部分的基于栈的执行引擎中会给出答案

4、代码是如何被 CPU 执行的

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

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

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

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

4.1、通用寄存器:AX,BX,CX,DX

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

4.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)指令指针寄存器存储下一条将要执行的指令的内存地址(此处的描述不够准确,下文解释)

4.3、段寄存器:CS,DS,SS

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

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

物理内存地址 = 段地址(如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.4、指令寄存器:IR

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

4.5、标志寄存器:FR

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 部分讲解

5、课后思考题

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

编译型语言有:C / C++、Pascal、Object-C、Swift、Golang 等
解释型语言有:JavaScript、Python、PHP、Ruby 等
混合型语言有:C#、Java

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

这样做的目的是方便程序员 debug 代码
比如,我们将 C 语言代码编译成汇编代码之后,通过阅读汇编代码,可以发现所编写的 C 语言代码有哪些值得优化的地方
posted @ 2023-05-11 15:16  lidongdongdong~  阅读(373)  评论(0编辑  收藏  举报