Loading

java代码从出生到执行的过程浅析

阅读《深入理解java虚拟机 第二版 JVM高级特性与最佳实践》 - jdk版本为1.6

1.什么是编译型语言、解释型语言

解释型语言:源代码不是直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。比如Python/JavaScript / Perl /Shell等都是解释型语言。

编译型语言程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次。像C/C++等都是编译型语言。

java是比较特殊的存在,java从源程序到真正执行需要经历从.java文件到.class文件的编译(前端编译,对应的编译器有javac,eclipse的ECJ),.class文件到二进制机器码的解释执行(后端编译,对应的编译器有JIT);还有一种能够直接把java文件编译成本地机器代码的静态提前编译器(AOT编译器)。

大致的执行路线如下图:

java语言的"编译期"分为前端编译和后端编译两个阶段。前端编译是指把*.java文件转变成*.class文件的过程; 后端编译(JIT, Just In Time Compiler)是指把字节码转变成机器码的过程。

2.Java中的前端编译

 

 

2.1词法分析

先弄懂一个概念:token:计算机科学中将字符序列转换为单词(Token)序列的过程

我们把执行词法分析的函数称为词法分析器。它的主要功能是将源代码的字符流转变为Token集合, Token则是编译过程的最小单位。

规范化的Token包含:

  • java关键词:package、import、public、class、int等
  • 自定义单词(标识符):包名、类名、变量名、方法名
  • 符号:=、;、+、-、*、/、%、{、}等

有一个例子,我们直接拿过来:

package compile;
public class Cifa {
    int a;
    int c = a + 1;
}

以上代码转化为的Token流:

 

我们可以看到每个token中的数据结构:token的类型及token的具体的值

  • Javac是如何分辨出一个个的Token

Javac进行词法分析时会根据java语言规范来控制什么顺序,在什么地方应该出现什么Token(例如对package的读取:在创建javacParsepackage对象的构造函数时,Scanner会读取第一个Token(Token.PACKAGE),而词法分析器的整个过程是在javacParser的parsrCompilationUnit方法中完成的,先判断当前的Token是不是Token.PACKAGE(是的话读取package的定义),接着读取下一个Token(IDENTIFIER),再读取类名时如果遇到Token.Dot也就是‘.’将继续往下读,直到读得完成类名即遇到Token.SEMI(“;”)为止)。由此可以看出,读取哪个Token是由javacParser规定的而Token流的顺序要符合java语言规范。

  • Javac如何确认一个Token的

如何确定字符组合是一个Token的规则是在Scanner的nextToken方法中定义的,每调用该方法一次就会构造一个Token,而这些Token必然是com.sun.tools.javac.parser.Token中的任何元素之一。

在读取每个Token时都需要一个转换过程(如在package中的"compile"包名要转化成Token.IDENTIFIER类型),在Java源码中的所有字符集合都要找到在com.sun.tools.javac.parser.Token中定义的对应关系,这个任务是在com.sun.tools.javac.parser.Keywords类中完成的,Keywords负责将所有字符集合对应到Token流中

2.2语法分析

  我们得到了token的集合之后,在语法分析器中,将进行词法分析后形成的Token流中的一个个Token组成一句句话,检查这一句句话是不是符合Java语言规范。

 每个语法树上的语法节点都是com.sun.tools.javac.tree.JCTree的实例,语法树的一些规则如下:

 

1.每个语法节点都会实现一个xxxTree接口,该接口继承自com.sun.source.tree.Tree接口。如IfTree语法节点表示一个if类型表达式
2.每个语法节点都是com.sun.tools.javac.tree.JCTree的子类并且会实现1中提及的接口类,这个类的类名类似于JCxxx类,如实现IfTree接口的实现类为JCIf
3.所有的JCxxx类都作为一个静态内部类定义在JCTree类中

JCTree类中有如下三个重要的属性

1.Tree tag:每个节点都会用一个整形常数表示,并且每个节点的类型的数值是前一个节点的类型数值加1
2.pos:也是一个整数,表示语法节点在源文件中的起始位置,文件的起始位置为0,-1的话表示不存在
3.type:表示这个语法节点是什么类型,如int、float还是String

 

2.3语法树解析

package解析:JCIdent=>JCFieldAccess

根据Name对象构建了一个JCIdent语法节点,如果是多级目录,将构建JCFieldAccess语法节点,JCFieldAccess语法节点可以是嵌套关系

import解析:JCIdent=>JCFieldAccess=>JCFieldAccess=>JCIdent

当成功解析出package语法节点之后,parseCompilationUnit()这个节点会调用importDeclaration()方法来解析得到import语法树。

importDeclaration()首先检查Token是不是Token.IMPORT,如果是则构造一个语法树,再匹配是不是有static关键字看看是不是静态引入。接着importDeclaration()就会调用语法解析器的Ident()方法解析出一个JCIdent语法节点,如果import语句中包含多级目录的时候,语法解析器就会调用Select()方法解析为嵌套的JCFieldAccess语法节点。

当语法解析器成功解析出JCIdent和JCFieldAccess节点之后,importDeclaration()方法会调用Import()方法,将之前解析的语法节点,整合成为一棵JCImport节点。

实际开发中,通常会有多个import关键字声明,那么importDeclaration()方法内部会通过迭代循环方法解析出多个JCImport语法树,然后将其存储在一个集合中。

class解析:

Import节点解析完之后就是累的解析(interface、class、enum),以class为例

package compile;

public class Yufa {

int a;
private int c=a+1;

public int getC(){

  return c;

}

public void setC(int c){

  this.c=c;

}

}

第一个Token是Token.CLASS这个类的关键词,接下来是用户定义Token.IDENTIFIER,也是类名。然后是参数,下一个是Token.EXTENDS或者Token.IMPLEMENTS,接着是对classbody的解析,这个classbody解析的结果保存在list集合中,最后将这些子节点添加到JCClassDecl这颗class树中。

这个类解析完成之后,会把这些子树加到顶层语法节点JCCompilationUnit(以package作为pid并且持有JCClassDecl语法节点的集合)之下,完整的语法树如下:

2.3语义分析

作用:将语法树转化为注解语法树

 

  • 添加默认的无参构造器(在没有指定任何有参构造器的情况下)
  • 处理注解
  • 标注:检查语义合法性、进行逻辑判断
    • 检查语法树中的变量类型是否匹配(eg.String s = 1 + 2;//这样"="两端的类型就不匹配)
    • 检查变量、方法或者类的访问是否合法(eg.一个类无法访问另一个类的private方法)
    • 变量在使用前是否已经声明、是否初始化
    • 常量折叠(eg.代码中:String s = "hello" + "world",语义分析后String s = "helloworld")
    • 推导泛型方法的参数类型
  • 数据流分析
    • 变量的确定性赋值(eg.有返回值的方法必须确定有返回值)
    • final变量只能赋一次值,在编译的时候再赋值的话会报错
    • 所有的检查型异常是否抛出或捕获
    • 所有的语句都要被执行到(return后边的语句就不会被执行到,除了finally块儿)
  • 进一步语义分析
    • 去掉永假代码(eg.if(false))
    • 变量自动转换(eg.int和Integer)
    • 去掉语法糖(eg.foreach转化为for循环,assert转化为if,内部类解析成一个与外部类相关联的外部类)
  • 最后,将经过上述处理的语法树转化为最后的注解语法树

2.4代码生成

 作用:将注解语法树转化成字节码,并将字节码写入*.class文件。

  • 将java的代码块转化为符合JVM语法的命令形式,这就是字节码
  • 按照JVM的文件组织格式将字节码输出到*.class文件中

在生成的*.class文件中不只包含字节码信息,具体包含:

  • 结构信息
    • class文件格式版本号
    • 各部分的数量与大小
  • 元数据
    • 类、父类、实现接口的声明信息
    • 属性声明信息
    • 方法声明信息
    • 常量池
  • 方法信息
    • 字节码
    • 异常处理器表
    • 局部变量区的大小
    • 操作数栈的大小
    • 操作数栈的类型记录
    • 调试用符号信息

 

 引用:
https://www.cnblogs.com/java-zhao/p/5194064.html
posted @ 2019-11-13 17:53  冯廷鑫  阅读(344)  评论(0编辑  收藏  举报