基础知识整理1:从HelloWorld开始理解javac前端编译过程

1、学习的方法——不在学多少在于融汇贯通知识点

    新知识的层出不穷,越早建立起自己的知识体系越重要,建立知识体系的关键在于如何找到知道的原点去学习,有了一个大概的全局观以后,剩下的都是一些基础知识点的细化。如果把开发一个项目或解决一个特定场景的解决方案比作一个体的话:

  • 点:基础的知识,相关的语法:如计算机原理、操作系统原理、底层IO、JVM、语言的语法逻辑
  • 线:相关的知识点,如反射、线程、网络编程、IO操作、分布式、sql优化、协议等原理性的知识等
  • 面:各种层出不穷的解决方案与框架,如Spring、Hadoop、Spark、MQ/Kafka等等
  • 体:具体的应用与项目,如解决的一些上层应用(olap、oltp、数仓、画像、推荐系统等等)

其中最重要的就是对点和线和掌握,点和线知识掌握越牢,后面学框架和做项目才能真正明白为什么要这么做,而不是一味模仿或凭经验感觉乱撞。所以越往上学习提升时,越底层的点和线和知识就显得越重要。

 

2、两种不同的编程方式

  • 面向过程:关注的是数据的流向,如C语言
  • 面向对象:关注的是不现对象之间如何进行交互,如C++和JAVA。Java相比起C++在于把复杂的语法、手动释放内存以及容易造成编程错误的指针等弊端给屏蔽了。

 

3、从Helloworld开始分析javac前端编译与优化

编译有三个不同的语境,不同的阶段不同的地方表示的不一样:

  • 前端编译:idea编辑器调用用JDK中的javac过程,即将源文件转换成class字节码文件过程
  • 即时编译:jvm中的hotspot虚拟机中的c1、c2在运行期把字节码转成本地机器码的过程
  • 提前编译:AOT编译,给即时编译做缓存加速——减少程序启动时间和性能的预热时间

即时编译与提前编译也被合称为后端编译。

javac前端编译过程:

Java虚拟机规范并未对Java源码编译过程做严格约束,也就给了Java前端编译器较大的灵活性,Javac大致分为1个准备和3个处理过程将源码转成字节码:

1)、准备过程:初始化插入式注解处理器,主要是针对Annotation的注解进行预处理

2)、解析与填充符号表过程,包括:

     a、词法,语法分析:将源码中的字符流转换为标记(Token)集合,构造出抽象语法树

          词法分析单个字符是程序编写时的最小单位,而标记是编译时使用的最小单位。关键字、变量名、字面量、运算符都可以作为标记。

                          如:public static int index = 100,就包含了6个标记,public static int分为3个关键字,1个变量名,一个运算符,1个字面量构成。

         语法分析根据标注Token系列构造语法树AST,树中的每一个节点都表示了程序中的一个语法结构,

                         如:包、类型、修饰符、运算符、接口、返回值、注释等

网上有人统计过有16类Java代码中最常用的元素,如:“包(PACKAGE)、枚举(ENUM)、类(CLASS)、注解(ANNOTATION_TYPE)、接口(INTERFACE)、枚举值(ENUM_CONSTANT)、字段(FIELD)、参数(PARAMETER)、本地变量(LOCAL_VARIABLE)、异常(EXCEPTION_PARAMETER)、方法(METHOD)、构造函数(CONSTRUCTOR)、静态语句块(STATIC_INIT,即static{}块)、实例语句块(INSTANCE_INIT,即{}块)、参数化类型(TYPE_PARAMETER,既泛型尖括号内的类型)和未定义的其他语法树节点(OTHER)

词法分析过程由com.sun.tools.javac.parser.Scanner

语法分析过程由com.sun.tools.javac.parser.Parser类实现,

抽象语法树是以com.sun.tools.javac.tree.JCTree类表示的

填充符号表的过程由com.sun.tools.javac.comp.Enter类实现

数据流及控制流检查过程由com.sun.tools.javac.comp.Flow类完成

解语法糖由com.sun.tools.javac.comp.TransTypes类和com.sun.tools.javac.comp.Lower类中完成。

生成字节码由com.sun.tools.javac.jvm.Gen类来完成

最后一步由com.sun.tools.javac.jvm.ClassWriter由这个类的writeClass()方法输出字节码,生成最终的Class文件

如以下代码,词法分析后再生成AST

package com.example.adams.astdemo;
public class TestClass {
    int x = 0;
    int y = 1;
    public int testMethod(){
        int z = x + y;
        return z;
    }
}

不同的分支上表示不同的语法结构: 

 

     b、填充符号表:生成符号地址和符号信息

符号表是由一组符号地址和符号信息构成的表格;

符号表中所登记的信息在编译的不同阶段都要用到,在语法分析中, 符号表所登记的内容将用于语义检查和产生中间代码。在目标代码生成阶段, 符号表是当对符号名进行地址分配时的依据。

这个结构比较抽象:可以把它理解为KV或类似栈之类的存储结构

在注解处理时可能 产生新的符号,又会回到之前的解析与填充符号表中重新处理这些新符号

3)、(插入式)注解处理器的注解处理:执行注解处理

主要是针对Annotation部分的重点处理

4)、语义分析:分析与字节码生成,包括:

(注意: 这一步如果是用idea这样手编辑器在编译前就会提前进行了处理,也就是编写时就提前进行了一部分处理,另一部分的会在编译时再进行处理)

    a、标注检查:对语法的静态信息进行检查

如变量使用前是否已被声明,变量与赋值之间的数据类型是否匹配以及做一些常量计算,如你与为3+2时,会直接变成5作为一个字面量。

    b、数据流及控制流分析:对程序动态运行过程进行检查、

对程序上下文逻辑进一步验证:

如:程序局部变量在使用前是否有赋值

方法的是否都有返回值

所有的异常检查是否有做了处理……

    c、解语法糖:将简化代码编写的语法糖还原为原有的形式,

语法糖主要是像Lambda语法、流式计算等新语法、以及一些泛型擦除、变长参数、自动装箱/拆箱,条件编译等,虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。

泛型:是为了保证类型正确被使用的,主要是针对编译前保证安全(说到底是给程序员用的,不是给机器用的),编译时会做语法检查,检查完成后自然也就没有必要再进行保留了。

    d、字节码生成:将以上过程生成的信息合成一个字节码文件。

不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换、优化工作。

例如,实例构造器<init>()方法和类构造器<clinit>()方法就是在这个阶段添加到语法树之中的(注意,这里的实例构造器并不是指默认构造函数,如果用户代码中没有提供任何构造函数,那编译器将会添加一个没有参数的、访问性(public、protected或private)与当前类一致的默认构造函数,这个工作在填充符号表阶段就已经完成),这两个构造器的产生过程实际上是一个代码收敛的过程,编译器会把语句块(对于实例构造器而言是“{}”块,对于类构造器而言是“static{}”块)、变量初始化(实例变量和类变量)、调用父类的实例构造器等操作收敛到<init>()和<clinit>()方法之中,并且保证一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行,上面所述的动作由Gen.normalizeDefs()方法来实现。除了生成构造器以外,还有其他的一些代码替换工作用于优化程序的实现逻辑,如把字符串的加操作替换为StringBuffer或StringBuilder的append()操作等。

完成了对语法树的遍历和调整之后,就会把填充了所有所需信息的符号表交给com.sun.tools.javac.jvm.ClassWriter类,由这个类的writeClass()方法输出字节码,生成最终的Class文件,到此为止整个编译过程宣告结束。

小结:前端编译的过程

java代码运行逻辑:java源代码(符合语言规范)-->javac-->.class(二进制文件)-->jvm-->机器语言(不同平台不同种类)

流程:

  • 词法分析器:将源码转换为Token流
    • 将源代码划分成一个个Token(找出java语言中的if,else,for等关键字)
  • 语法分析器:将Token流转化为AST语法树
    • 将上述的一个个Token组成一句句话(或者说成一句句代码块),检查这一句句话是不是符合Java语言规范(如if后面跟的是不是布尔判断表达式)
  • 语义分析器:将语法树转化为注解语法树
    • 将复杂的语法转化成简单的语法(eg.注解、foreach转化为for循环、去掉永不会用到的代码块)并做一些检查,添加一些代码(默认构造器)
  • 代码生成器:将注解语法树转化为字节码(即将一个数据结构转化成另一个数据结构)

ps:要获取javac编译器,可以通过OpenJDK来下载源码,可以自己编译javac的源码,也可以通过调用jdk的com.sun.tools.javac.main.Main类来手动编译指定的类。Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,代码逻辑集中在这个类的compile()和compile2()方法中,整个编译最关键的处理就由图中标注的8个方法来完成

 

下一篇:分析生成的Class文件的结构 

参考:《深入理解Java虚拟机》


版权声明:本文为qq_36269641原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>