javac编译器工作原理(1)高级语言的编译原理
众所周知,Java语言有自己的语言规范,这个规范描述了Java语言有那些词法和语法的,有繁多的关键字,有固定的语法,而Java语言本质意义上是一门高级语言,这种语言如何被计算机理解并由CPU执行,就要倚靠JVM和JVM内置的编译器来完成。
首先我们来了解一下,高级语言是如何编译成为低级语言并由计算机执行的
计算机是不能直接运行高级语言代码的,CPU识别的只有二进制的0和1,而早期的工程师们就定义了不同的0和1组成了机器码(Machine code)
机器码也叫做机器语言,是可以由计算机的中央处理单元(CPU)直接执行的一组指令。
每条指令执行一个非常特定的任务,例如对CPU寄存器或内存中的数据单元执行加载,跳转或ALU操作。
每个由CPU直接执行的程序都由一系列这样的指令组成。
而高级语言就需要把语言本身经过某种手段,转化为机器能识别的机器码,然后交由操纵系统处理,最终CPU才可以识别运行
这里我们以C语言的汇编过程为例。
可以看出,C语言代码的编译过程分为四步:
预处理->编译->汇编->链接
假设有一个C语言源文件 my.c
#include<sdtio.h>
int main(){
printf("test");
}
在Linux使用gcc编译器,进行编译(注意此时直接完成了预处理和编译,得到了汇编代码)
gcc -S my.c
编译完成的汇编代码如下:
.file "my.c"
.section .rodata
.LC0:
.string "test"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.2.0-8ubuntu3.2) 7.2.0"
.section .note.GNU-stack,"",@progbits
而这些汇编代码,经过第三步汇编生成二进制01机器码,机器码可以被CPU理解执行,这里说一下,汇编代码也只是机器码的一种语言表述,和机器码具有一一对应的关系
关于什么是汇编代码–维基百科
最后经过第四步,动态或者静态链接完成,生成的可执行的二进制代码即可由操作系统执行。
如果对上面的图和我说的还是不太理解,可以参考阮一峰大神的博客编译器的工作过程,值得一提的是对于不同的CPU架构,其汇编代码(Assembly Code)以及对应的机器码(Machine Code)是不尽相同的,比如x86汇编代码和ARM汇编代码,主要原因是因为不同CPU设计寄存器数目的不同等等。
而JVM的设计其实就和物理机一样,具有自己的堆、栈和执行引擎以及PC计数器
Java虚拟机就像物理机一样,同样是无法看懂Java代码的,所以就需要某种编译器将Java代码翻译成虚拟机能看懂的代码,类似物理机上CPU能读懂的机器码一样。JVM这种设计使得JVM这个平台更具开放性,无论是Java语言,还是JVM上的新成员Kotlin,Groovy等,最终还是要被某种编译器翻译成JVM能读懂的字节码。这些字节码,相当于物理机器能识别运行的机器码(或者对应的汇编语言)
而经过javac编译生成的.class文件,其实就是相当于编译过程中产生的汇编文件。
参考资料:
《深入分析Java Web技术内幕》–许令波
《深入理解Java虚拟机》–周志明
C语言编译过程详解
Java内存管理:深入Java内存区域