学习于:https://www.bilibili.com/video/av17649289/?p=1
参考:《深入理解java虚拟机》
什么是编译?
我们知道,计算机语言一般被分为3种,高级语言,汇编语言,机器语言。
机器语言是一串二进制序列,全部是由0或1组成。能直接被计算机识别。
如:
![c416ab788e95a4f85c1e18e3ef12ff0e.png](https://i-blog.csdnimg.cn/blog_migrate/1c1d552a63960b4142c9162bcdabf077.png)
c706是操作码,整个语句的意思是将数值2存放到地址0000的地方。这里是使用16禁止表示。
机器语言与人类表达习惯想去甚远,难以记忆,难编写,难阅读,易写错。
于是,很快出现了汇编语言。
汇编语言引入了助记符,比如下面的MOV就是一个助记符。语义和前面的一样,也是把数值2写入到地址X当中。(这里X应该可以是一个寄存器,视频没说)
![37eaca216a4018417f1a31c81aa3fbd7.png](https://i-blog.csdnimg.cn/blog_migrate/741c8658fd66f532db09471fb515d974.png)
这里引入助记符后,更加直观。但是,汇编语言依赖于特定机器,程序员需要熟悉目标机的特性,非计算机专业人员使用受限制。而且汇编语言的编写效率比较低。即使是一个比较简单的数学表达式也需要好几条指令。
于是,就出现了高级语言。
高级语言以一种类似于数学定义或自然语言的简洁形式来编写程序,完成前面的目的的话只需要 x=2即可。
高级语言接近人类表达习惯,不依赖于特定机器,编写效率高。
高级语言和汇编语言都要变成机器语言才能被计算机识别。
具体什么是编译,什么是汇编,如下图:
![8fca85b25373d22a0ab77c4263d31fc2.png](https://i-blog.csdnimg.cn/blog_migrate/ba5e86c92df4321bcabf78846aab5215.jpeg)
简而言之就是:编译就是高级语言翻译成汇编语言或机器语言的过程。 前者称为源语言,后者称为目标语言。
编译原理就是学习如何将高级语言编译成汇编语言或机器语言的,也就是说怎么写一个编译器。
然后来看一下语言处理系统的结构:
![28008bdd25a61f6e7c4d0dd4518ff010.png](https://i-blog.csdnimg.cn/blog_migrate/8838e0dad6b8007bcc4952b5fdc225bb.jpeg)
源程序可能被分成不同的模块被放在不同的文件当中。
预处理器就是把存储在不同文件中的源程序聚合在一起。把被称为宏的缩写语句转换为原始语句。
经过预处理的源程序经过编译器和汇编器后就变成了可重定位的机器代码。
可重定位:在内存中的起始位置是不固定的。
代码中的所有地址都是相对于这个起始地址的相对地址,起始位置+相对地址=绝对地址。感觉这个有点操作系统知识的都比较好理解。
然后,加载器的作用就是修改可重定位地址;将修改后的指令和数据放到内存中适当的位置。
大型程序经常被分割为多个部分进行编译,因此,可重定位的机器代码通常要和其它和重定位的机器代码或者库文件通过链接器进行链接,生成可执行代码。
链接器:
1.将多个可重定位的机器代码文件(包括库文件)连接到一起
2.解决外部内存地址问题。(也就是一个文件引用了另一个文件的对象或者地址,这对于这个文件来说就叫外部地址)
从上面就看可以看出编译器在整个语言处理系统中的位置。
(看完感觉这老师一句废话都没有。。)
接着来看编译系统的结构:
先是要弄清编译器是如何将高级语言转化成汇编语言或者机器语言的。
首先,我们要处理的是源语言句子,第一步就是分析源语言,然后得出句子的语义。第二步:生成目标语言
其中,分析源语言的过程就叫 “语义分析”。
视频的老师用英语转汉语的角度来分析:
![c15682e1bfd902144f28c51286f3f244.png](https://i-blog.csdnimg.cn/blog_migrate/8a1188cfad0119a87e092811a9aedd58.png)
通过谓语的主动语态和被动语态,来得出施事者和受事者,然后分析出状语和补语。然后得出关系图,然后就可以转化为目标语言了。
然后要进行“语义分析”,首先要划分句子成分,如下图,状语和补语通常都是介词短语,所以可以进行判断。
![925f41c7ed781ef7dc0056d71b69f50a.png](https://i-blog.csdnimg.cn/blog_migrate/b21a3c222a41eb0baa69f168cc7571fe.png)
而识别出句子中各类短语的过程,又叫“语法分析”。
我们又是根据什么来识别句子中的各类短语呢?
是通过 词性 。如下图:比如说一个冠词加上名词,就可以构成一个名词短语。
![5585116d065f4dad57cd80521ade3cf7.png](https://i-blog.csdnimg.cn/blog_migrate/2973a93e3549c1956c3248da6e88e9c4.jpeg)
而确定它的词性的过程,比如确定这个是代词还是冠词,还是名词,这个过程就叫词法分析。
综上所述,
首先,我们要进行”词法分析“,得出里面各个单词是什么类型的单词,是介词,是冠词,还是名词?
然后,在”词法分析“的基础上,进行”语法分析“,即得出各个短语,比如冠词加名词是名词短语,代词也是名词短语,然后介词加冠词加名词得出的是介词短语,经过语法分析,我们就可以得出各种短语。也就是句子的结构。
然后在”语法分析“的基础上,再进行”语义分析“。比如就是知道broke是动词短语并且是主动形式,那么就知道前面的主语,后面的是宾语。然后就可以得出关系图,有了关系图后就可以转化为目标语言了。
(这里有些是我个人的理解,不恰当的地方还请大家指正,建议观看视频)
可以参考下图理解:
![23707d0c2e9353292ac9cd837019f34a.png](https://i-blog.csdnimg.cn/blog_migrate/c4397979c995037ddf68bfb14bea82bb.jpeg)
下面的得出的中间表示形式,也就是可以转化为各种语言的。
下面这个图很不错:
![c00133d259a179c1e6858cc934131cf2.png](https://i-blog.csdnimg.cn/blog_migrate/f51cb82e74c81ca635c86b3d65c6345d.jpeg)
然后结合上面的理解,来看具体的词法分析,语法分析和语义分析。
词法分析/扫描:
词法分析的主要任务:
从左到右逐行扫描源程序的字符,识别出各个单词,确定单词的类型。将识别出的单词转换成统一的机内表示---词法单元(token)形式.(也就是类似英语种字符转成单词的意思,token就是计算机语言里的单词,即最小单位)
token:<种别码,属性值>
![8ac47b984f418ad1e582247d524b03ee.png](https://i-blog.csdnimg.cn/blog_migrate/8e63dde23986824023b353e65967dd79.jpeg)
如上图,具体就比如两个整型常量 token:<int,3> token<int,4>
关键字: token:<if码,-> token:<else码,->
基本上一看就懂了。
来看老师的例子:
![f3ed836e3a26d8ad4d5cf2eecdf1e482.png](https://i-blog.csdnimg.cn/blog_migrate/76f67af2781e736673b70205bb95e560.jpeg)
有没有感觉很实在,哈哈。注意,种别码本身应该是一个整数,这里为了直观体现,我们采用了宏定义的形式。 那么,如何实现词法分析器呢?可以参考:https://www.cnblogs.com/yanlingyin/archive/2012/04/17/2451717.html 进行简单了解或者观看完后续视频。这里就不进行讲解了。
语法分析:
语法分析器(parser)从词法分析器输出的token序列中识别出各类短语,并构造语法分析树(parse tree) 。如下图:
![6ca8cbf9b879d5da0248abf564a52540.png](https://i-blog.csdnimg.cn/blog_migrate/5653d59755c1071ede25b459a6e9628e.jpeg)
语法分析树描述了句子的语法结构。
然后我们来看一下赋值语句的分析树:
如下图:先得到赋值语句的token序列。
![ae3c89e38c3b8b0447e65f43fa639eb1.png](https://i-blog.csdnimg.cn/blog_migrate/c7d702301d82b78473970cc5dfa322b4.png)
然后根据这个序列得到语法分析树:
![4920377b6b97f329e2e07da2fbe8fe1a.png](https://i-blog.csdnimg.cn/blog_migrate/dbbe94c739cee33078aa076451e3557b.jpeg)
可从右往左看,标识符组成表达式与标识符通过*号连接,然后再与表达式通过+号连接,然后再与标识符通过 = 号组合成赋值语句。
然后我们再来看看变量声明的语法树。
先来看文法: D ,即describe声明,T ,即 type IDS 即identity symbol 标识符 如下图
![cc62d13b4957de8ee7cc2a903a7ddc8a.png](https://i-blog.csdnimg.cn/blog_migrate/79e17525d9029eddf7de500f8a6e8d60.jpeg)
id或ids,id还可以继续组成ids,也就是说 int a; int a,b; int a,b,c; 都是可以的。
然后我们来看它的语法分析树:
![93fd0f37baf7bedc391788b1e509a5fb.png](https://i-blog.csdnimg.cn/blog_migrate/dff58767c345983a9fd3e5d93699103d.jpeg)
就这样从下面的token组成上的D,即我们前面说的短语。声明变量就是一个短语。和前面比较的名词短语,介词短语啥的是一样一样的。 那么,如何根据语法规则为输入句子构造分析树呢? 欢迎观看后续视频或者自行了解。这里不进行讨论。
然后我们来看看语义分析的主要任务:
语义分析分属性声明和执行语句
先来看对于属性声明而言语义分析:
收集标识符的属性信息
1.种属(kind) 即简单变量,复合变量(数组、记录……)、过程、……
2.类型(type) 如整型,字符型,指针型,布尔型……(也就是指标识符对应的变量是什么类型的)
3.属性的存储位置和长度 ,比如:
![70f42729bea70c70d1bbbee4ab91c52d.png](https://i-blog.csdnimg.cn/blog_migrate/ea19abb6a8a1c50f685f2094357b0019.png)
,假设real是8字节,int是4字节 。然后它的存储应该是这样的
![8f4be43771ffca4a46d3fafb75a8cb12.png](https://i-blog.csdnimg.cn/blog_migrate/fc66e4527f7635cc6556b91a22aa4245.jpeg)
4.值
5.作用域
6(对于过程来说还有参数和返回值信息,比如参数个数,参数类型,参数传递方式,返回值类型等)
生成之后,使用符号表利用数据结构来存储这些属性信息,如下图:simple 6个字节,table 5个字节。
![823ecf645865adc9b972154e6fbb2c42.png](https://i-blog.csdnimg.cn/blog_migrate/2d977c5eb4e9be207f6998b1ef515794.jpeg)
接着还有语义错误等信息:
![4c8da111bc84e87ef8bc0f100b214b56.png](https://i-blog.csdnimg.cn/blog_migrate/c489110ed07330e99ea1a39f14950e2a.jpeg)
运算类型不匹配,就比如整型和浮点型相加,自动将整型转成浮点型这类的东西。
当出现这些错误时就报错。
说实话,这部分看得不是很懂,想着根据前面的理解,应该是要表达出一句话的意思来着。这里是着重于属性信息的存储和检查错误。
后面还有生成中间代码和后端编译器,在这里就不讲了。
嗯,感觉编译原理基础部分就到这里吧,后面的以后有机会再进行深入学习。然后来看Java的编译。
java语言的”编译期“其实是一段不确定的操作过程,因为它可能是指一个前端编译器把 .java文件转变成.class文件的过程;也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程;还可能是指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler)直接把.java文件编译成本地机器代码的过程。
和我们前面讲的非常相似的就是前端编译器,即.java文件转变成.class文件的过程,也就是我们平常使用的javac命令。
看sun的javac的源码,编译大致分为三个过程:
1.解析与填充符号表
2.插入式注解处理器的注解处理过程(注解处理后可以返回1,继续解析与填充符号表,也可以到3)
3.分析与字节码生成过程
先看解析与填充符号表:
它可分为两步:1.词法、语法分析
2.填充符号表
1.词法语法分析
词法分析是将源代码的字符流转变成标记(token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符都可以成为标记,如“int a=b+2”这句代码包含了6个标记,分别是int、a、=、b、+、2,虽然关键字int由3个字符组成,但是它只是一个token,不可再拆分。
语法分析是根据token序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree,AST)是一种用了描述程序代码语法结构的树形表示方法,语法树的每个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、代码注释、返回值等都可以是一个语法结构。
2。填充符号表
完成词法和语法分析之后,下一步就是填充符号表的过程。符号表(symbol table)是由一组符号地址和符号信息构成的表格。符号表中所等级的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
然后注解处理:
理解后是对注解的支持使用,注解与普通的Java代码一样,是在运行期间发挥作用,可以读取修改,添加抽象语法树中的任意元素。(我理解着这是广义上的语法分析)
最后是语义分析和字节码生成
语法分析后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查。观察下图:
![d2e7c76716a820de39e9954597602ae2.png](https://i-blog.csdnimg.cn/blog_migrate/f6470500d539bf2ebb86d7ab99cbb17b.jpeg)
(这个和前面的编译原理举的例子一模一样。。简直天秀)
语义分析分为标记检查以及数据及控制流分析两个步骤。
1.标记检查
分析变量使用前是否被声明、变量与赋值之间的数据类型是否能够匹配等。在这里有一个值得注意的地方:常量折叠。 比如 int a=1+2; 语法树上仍然能看到字面量1、2、+,但是经过常量折叠后,他们会被折叠成字面量3。由于编译期间进行常量折叠,所以在代码里定义a=1+2,比起直接定义a=3,并不会增加程序运行期哪怕仅仅一个cpu指令的运算量。
2.数据及控制流分析
对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值
方法的每条路径是否有返回值、是否所有的受查异常都被正确处理了等问题。
3.解语法糖
我的理解就是添加一些功能。比如支持泛型、变长参数、自动拆箱装箱等。
4.生成字节码文件
把前面各个步骤所生成的信息(语法树,符号表)转化成字节码写到磁盘当中,编译器还进行了少量的代码添加和转化工作。
总结下来的话:
java编译器的话经过四个步骤:词义分析,语义分析,语法分析和字节码生成。
然后来看看Java的后端编译器(也就是class文件到运行的过程)
JIT编译器:
Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“热点代码”(hot spot code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器( Just In Time Compiler , JIT编译器)
JIT编译器编译性能的好坏、代码优化程度的高低是衡量一款商业虚拟机优秀与否的最关键的指标之一。
然后感觉需要先了解一下“解释执行”和“编译执行”这两个概念先。
参考:https://www.bilibili.com/video/av46283307?from=search&seid=13298237907087607679
解释执行:把代码解释一行,然后执行,执行完,解释结果就丢了。然后需要的话再解释,再执行。
编译执行:一次性翻译好了,就放在这里,后面的多次执行也不需要重新进行编译了。除非程序修改了就需要重新编译。
所以,编译执行的效率要高于解释执行。它直接执行就完了,不用再解释了。
然后可以再参考 :https://baike.baidu.com/item/%E7%BC%96%E8%AF%91%E6%89%A7%E8%A1%8C
https://baike.baidu.com/item/%E8%A7%A3%E9%87%8A%E6%89%A7%E8%A1%8C
进行更详细的了解。
然后,class文件转化为各种操作平台上的机器码的时候,一般来说,都是通过JVM内嵌的解释器将字节码转化为最终的机器码,然后在该操作系统上执行。这部分就是解释执行了。可是,我们前面也介绍了JIT编译器,它在运行时将“热点代码”编译成机器码。这里就属于编译执行了。 所以,不能单纯地说Java时解释执行还是编译执行。
解释器与编译器各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率。
然后关于 “如何判定为热点代码”的问题:这里时有个方法调用计数器和回边计数器,当她两之和大于一个阈值就被当作热点代码。即 一个Java方法,要运行的时候,首先查看是否存在编译版本,如果是,就执行编译后的本地代码版本。如果否,就方法的调用计数器值加一。当那两个计数器值之和超过阈值,则让编译器编译,否则让解释器直接解释执行该方法。如下图:
![c185ab81f10d7f734816bb7405554122.png](https://i-blog.csdnimg.cn/blog_migrate/f2c46619784303c0ca9b3e182322c6fa.jpeg)
(此图来源于《深入理解Java虚拟机 》 第334页)
这个叫基于计数器的热点探测方法。在hotspot虚拟机中使用。
所以,Java的说的什么“一次编译,到处执行”,这个编译时Java文件到class文件的编译。并不是JIT编译。JIT编译算作Java的二次编译了。
嗯,就记到这里吧,都比较基础。
欢迎交流讨论。