前言

首先问大家一个问题,我们开发好 Java 代码是如何运行的?我们写了这么多年的代码,对于代码运行的流程是否清楚呢?

是不是在 ide 中点一下 Run 启动就完事了,我们写的代码直接就跑起来了,其实在背后编译器和虚拟机默默的在帮我们承受了这一切。

Java 程序从源文件创建到程序运行要经过两大步骤:

1、源文件由编译器编译成字节码

2、字节码由 Java 虚拟机解释运行。因为 Java 程序既要编译同时也要经过 JVM 的解释运行。

Java 代码编译

代码编译由 JAVA 源码编译器来完成。主要是将源码编译成字节码文件(class文件),字节码文件格式主要分为两部分:常量池和方法字节码。

Java 代码编译是由 Java 源码编译器来完成,流程图如下所示:

类执行机制

Java 字节码(class 文件)的执行是由 JVM 执行引擎来完成,流程图如下所示:

从这里我们可以看出来,当我们点击 Run 启动一个项目时,背后却是很多组件共同在努力的结果,所以我们才能看起来毫不费力。

Java 代码想要运行起来,第一步就要得到编译器的认可。编译器的任务很简单,就是将符合 Java 语言源码编译为符合 Java 虚拟机规范的 Class 文件,如果输入的 Java 源码不符合规范则需要报告错误。

所以我们想了解编译器就必须了解 JDK 与 JRE

JDK 与 JRE

那么 JDK 与 JRE 安装包跟 Java 又有什么关系?

这里我们理解清楚所谓的 JDK 和 JRE 到底有什么区别,接下来看一下 Java 8 的体系架构图:

1、JRE(Java Runtime Environment)是 Java 运行时环境,它是运行已编译 Java 程序所需的所有内容的集合,包括Java虚拟机(JVM),Java 核心类库和一些基础的构件。

2、JDK(Java Development Kit)是 Java 的开发工具包,它不仅提供了 Java 程序运行所需的 JRE,还提供了一系列的编译,运行等工具,如 javac,java,javaw 等。

通过了解这两个包,我们可以明白,JDK 中包含了 JRE,JRE中又包含了 JVM,它们三个的关系:JDK》JRE》JVM,所以我们在安装 JDK 时,通常不需要考虑 JRE,JVM 之类的,只要你安装好了JDK,其他两个就都有了。

但是,如果我们是普通用户,并不需要关心开发,甚至是不懂代码,我只想要代码跑起来的结果,那只需要本地有 JRE 运行环境就行了。

那么这里大家会有一个疑问了,既然我们安装 JRE 就能运行 Java 代码,但为什么需要 JDK 才能开发呢,为什么缺少这个就只能是运行环境,而不能进行开发?原因就在如下图所示的部分:


这里我们只需要注意 javac 这个命令就好,javac:用于编译 Java 源代码,生成 class 文件。

这个 javac 是 JDK 中内嵌的编译器,通过这个命令,可以将 java 源文件转换成 class 文件。这个 javac编译器就是 JRE 相比于 JDK 少了开发功能的关键原因。

下面我们以开发好的 Java 代码在完整的 JDK 架构下,经过JDK、JRE 以及 JVM 的运行过程分析是怎么实现的,如下图:


从上图我们可以看到,通过 JDK 中的 javac 命令才能将 java 源代码编译成 class 文件,而 class 文件最终放到 JVM 中运行。

这里我们把 java 源码到 class 文件的过程称为编译阶段,把 class 文件到 JVM 中运行得到结果的阶段称为运行阶段。因此,如果只有 JRE 而没有完整的 JDK 的话就少了编译源代码的关键工具,只能依赖别人已经编译好的 class 代码,将程序运行起来,而不具备开发的能力。

所以这里 JVM 对于使用什么语言并不关心,JVM 运行需要的是 class 文件,只要生成 JVM 能够识别的字节码就好了。

编译阶段

当我们使用 javac 命令编译代码,将 .java 文件编译成了 .class 二进制文件。那么,在编译器中,源代码是怎样变化的呢,接下来我们就一起看一张编译流程图:

1、词法分析器:将源码转换为 Token 流
将源代码划分成一个个Token,找出 java 语言中的 if,else,for 等关键字。

2、语法分析器:将 Token 流转化为语法树
将上述的一个个 Token 组成一句句代码块,检查这一句句代码块是不是符合 Java 语言规范,比如 if 后面跟的是不是布尔判断表达式

3、语义分析器:将语法树转化为注解语法树
将复杂的语法转化成简单的语法并做一些检查,添加一些代码。

4、代码生成器:将注解语法树转化为字节码,即将一个数据结构转化成另一个数据结构。

词法分析器

词法分析是最开始的一步,主要的作用就是把源代码的字符流转换成 Token 集合,Token的类型包括:关键字、标识符、字面量、操作符、界符等,比如 int 就会被识别为 Token.INT ,运算符也会被分配为对应的 Token 类型,例如 + 就是 Token.PLUS

比如下面的源代码文件,经过词法分析器识别出的 Token 有:package、compile、public、class、int、foo、a、b、=、+、return、(){}等等Token

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

以上代码转化为 Token 流如下图所示:

语法分析器

当代码被解析为一系列的 Token 集合之后,下一步是进行语法分析,语法分析阶段把 Token 串,转换成一个体现语法规则的、树状数据结构,即抽象语法树 AST 。AST 树反映了程序的语法结构。

比如int a = b+1这一段代码,对应的 AST 抽象语法树如下所示:

生成 AST 以后,程序的语法结构就很清晰了,根据这个结构,可以层级地展示代码中所有的变量、方法甚至是注释等各种信息。

构建 AST 的过程会判断 Token 的类型与其在树中的位置是否匹配,这里很好理解,如果我们使用关键字作为变量名称的时候编译会不通过,就是在这一步被识别到了。如下所示用关键字定义变量:

public class myTest {public static void main(String[] args) {String class = "Hello world";}
}

因为 class 是关键字,构建语法树的时候发现关键字出现在标识符的位置,这样怎么行呢,所以会报错误出来,大家可以自己定义一个关键字看报错。

语义分析

语义分析跟第上面的词法分析、语法分析看起来很像,其实是有很大区别。

语法分析之后,编译器获得了项目代码的抽象语法树表示,语法树能表示一个结构正确的源代码的抽象,但无法保证源代码是符合逻辑的。而语义分析的主要任务是对结构上正确的源代码进行上下文有关性质的审查,例如变量是否已经声明,变量的数据类型与其参与的运算是否匹配等等。

下面举个例子,假设有如下的 3 个变量定义语句:

int a = 1;
boolean b = false;
char c = 2;

后面可能出现的赋值运算:

int d =a + c;
int e = b + c;
char f = a + c;

这一段代码能够通过第一步的词法分析和语法分析,并构成正确的 AST,但是在语义分析中会报错。因为编译器发现变量 e 和 f 的运算都是不符合规范的,参与运算的两个值的类型不匹配该运算符的逻辑。

语义分析更进一步检查上下文中变量的规范性,如果需要对语义分析做细分的话,主要包含两个步骤:

1、标注检查

2、数据流分析

标注检查

标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。在标注检查步骤中,还有一个重要的动作称为常量折叠,如果我们在代码中定义如下:

int a = 1 + 2;

上述代码我们在语法树上可以看到字面量1、2以及操作符 +,在经过常量折叠步骤以后,会生成一个新的字面量 3,在程序运行时 a 的值就是 3,不会再消耗 CPU 进行计算。那么在语法树上仍然能看到字面量 1、2 以及操作符 +,但是在经过常量折叠之后,它们将会被折叠为字面量 3,当后续到虚拟机中去执行字节码的时候,由于编译期常量折叠的优化,int a = 3 和 int a = 1 + 2 的运行效率其实是一样的,因为这一个常量的运算在编译期已经做完,不会再额外消耗运行期的处理时间。

数据流分析

数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检测出诸如程序局部变量是在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。

值得注意的是,final 变量不可重复赋值的性质也是在这一步检查,如果一个 final 变量被重复赋值,编译器会发现并报错的。也正是因为这个特性,用 final 关键字局部变量只会在编译期去校验,不会对在运行期产生任何作用 。

下面举一个关于 final 修饰符的例子:

public void food(final int avg) {  final int age = 0;  // do something
}  // 方法而没有 final 修饰
public void food(int avg) {  int age = 0;  // do something
}

在这两个 food() 方法中,第一种方法的参数和局部变量定义使用了 final 修饰符,而第二种方法则没有,在代码编写时程序肯定会受到 final 修饰符的影响,不能再改吧 avg 和 age 变量的值,但是这两段代码编译出来的 class 文件是没有任何一点区别的。

所以我们可以知道,所有的 final 不可重复赋值的限制,都在编译期得到了检验,如果声明为 final 的局部变量被重复赋值,在编译期就会报错,如果没有发现有 final 重复赋值的错误,才会成功生成字节码。

解语法糖

简单地来说,语法糖就是方便程序员编写的便捷写法,这种语法不会对最终的结果产生实际影响,但能够减少程序编写者的工作量。

例如,Java 中的自动拆箱装箱功能、foreach 循环功能等,都是为了程序员能够更写出更简洁流程的代码而封装的语法糖。

但是 Java 虚拟机并不支持这些语法糖,这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。

例如,将包装类型拆成普通类型,将增强 for 循环替换为普通的 for 循环。

字节码生成

字节码生成是 javac 编译过程的最后一个阶段,在 javac 源码里面由 com.sun.tools.javac. jvm.Gen 类来完成。

字节码生成阶段前面各个步骤所生成的语法树、符号表等信息转化成字节码指令写到 class 文件中,除此之外,编译器还进行了少量的代码添加和转换工作。比如我们反编译字节码文件看到的实例构造器 () 方法和类构造器 ()方法,就是这个阶段添加到语法树中。

1、类构造器 ():只对类方法 static 域和 static 代码块进行初始化, 方法是在:加载 -> 链接 -> 初始化的初始化阶段进行的。Java 在编译之后会在字节码文件中生成 方法,将静态域和静态代码块收敛到 方法中,收敛顺序为

  • 父类静态变量初始化
  • 父类静态语句块
  • 子类静态变量初始化
  • 子类静态语句块

2、实例构造器 ():在对象被创建(new)时,进行实例化操作的方法。并且子类的 方法会首先调用父类的 方法。Java 在编译之后会在字节码文件中生成 方法,该实例构造器会将变量,语句块初始化,调用父类的构造器等操作收敛到 方法中,收敛顺序为:

  • 父类代码块
  • 父类构造函数
  • 子类变量初始化
  • 子类代码块
  • 子类构造函数
  • 父类变量初始化

方法的的执行时间一定早于,因为 方法是在类初始化过程中执行的,而 方法是在对象实例化时执行的,因此它们完整的执行顺序就是:

  • 父类静态变量初始化
  • 父类静态语句块
  • 子类静态变量初始化
  • 子类静态语句块
  • 父类变量初始化
  • 父类语句块
  • 父类构造函数
  • 子类变量初始化
  • 子类语句块
  • 子类构造函数

Java 代码能够保持加载顺序的原因就是在生成 class 文件时,将按顺序拼接好的方法添加到了 class 文件中,在后续的运行过程中再按顺序执行。

至此,Java源代码到 class 文件的编译过程就讲完了,可能有漏的地方,但是重要的、大致的方向就这些了。

不能不懂的 Java 源代码编译过程分析相关推荐

  1. java 源码分析_Java 源代码编译成 Class 文件的过程分析

    原标题:Java 源代码编译成 Class 文件的过程分析 在上篇文章< Java三种编译方式:前端编译 JIT编译 AOT编译 >中了解到了它们各有什么优点和缺点,以及前端编译+JIT编 ...

  2. Java 代码编译和执行的整个过程

    一.简述 Java代码编译和执行的整个过程包含了三个重要的机制: ①Java 源码编译机制: ②类加载机制: ③类执行机制 二.Java 源码编译机制 Java 代码编译是由 Javac 编译器来完成 ...

  3. java代码编译过程_Java代码编译和执行的整个过程

    一.简述 Java代码编译和执行的整个过程包含了三个重要的机制:①Java源码编译机制:②类加载机制:③类执行机制 二.Java源码编译机制 Java代码编译是由Javac编译器来完成,流程如图: J ...

  4. “Java是编译执行的语言”这句话对吗?

    现在让你谈谈对Java平台的理解,你是否会感觉内容过于庞大?这个问题是比较宽泛的,Java发展到现在已经不仅仅是语言这么简单了,Java平台涉及的,包括但不仅限于下面提到的这些内容: Java语言本身 ...

  5. java反编译工具_如何保护Java程序 才能防止Java反编译

    简介:Java是一种跨平台的.解释型语言.Java 源代码编译中间"字节码"存储于class文件中.Class文件是一种字节码形式的中间代码,该字节码中包括了很多源代码的信息,例如 ...

  6. 强烈推荐的一款 Java 反编译工具:JAD

    01.JAD 是什么 JAD 是一款老牌的.经典的.使用起来简单的 Java 反编译工具.大家都知道,将源代码转换成二进制执行代码的过程叫"编译",比如将 Java 源代码编译成 ...

  7. 一些Java反编译工具/源代码查看工具的介绍

    2019独角兽企业重金招聘Python工程师标准>>> 有的朋友抱怨他们在使用他们公司的闭源框架时看不到底层的源代码.那么可以尝试使用一些Java反编译工具. 下面我举个例子介绍具体 ...

  8. Android APK反编译得到Java源代码或资源文件

    一.反编译Apk得到Java源代码 首先要下载两个工具:dex2jar和JD-GUI,前者是将apk中的classes.dex转化成Jar文件,而JD-GUI是一个反编译工具,可以直接查看Jar包的源 ...

  9. 将DEX反编译成Java源代码

    本文翻译自:decompiling DEX into Java sourcecode 如何将Android DEX(VM字节码)文件反编译成相应的Java源代码? #1楼 参考:https://sta ...

最新文章

  1. 问题小结(2)-dialog内容动态变化(调用系统方法时)
  2. python有没有类似unity3d_像web一样使用python
  3. 删除桌面图标的小箭头
  4. Python单元测试框架——unittest
  5. 前端实战项目:vue+elementUI管理平台
  6. 上班工作压力太大?来玩个游戏解压下
  7. 微信小程序 抓css,微信小程序css篇
  8. 选择部门-选择员工(js)
  9. sql2000个人版semobj.rll有病毒
  10. python基础分析_数据分析之Python基础
  11. 如何恢复手机删除数据文件
  12. php验证电子邮件,如何在PHP中验证电子邮件地址
  13. Hadoop IO 特性详解(1)【数据完整性】
  14. windows中 FFmpeg 配置libx264 遇到的坑和解决办法
  15. React.js 学习
  16. python列表比较方法_python列表常用方法
  17. 二十八、K8s最小服务漏洞2-OPA
  18. 我的springBoot框架学习总结
  19. 基于hibernate的泛型Dao层设计
  20. 智能合约BSC添加流动性

热门文章

  1. 学习笔记:python游戏脚本1.0版本,实现自动点击、识图、识别价格、弹窗提示低于预期价格可以购买
  2. GitHub 安装及使用
  3. 基于51单片机ds1302时钟、ds18b20、lcd12864的恒温器
  4. OC和swift混合工程更新库时报:target has transitive dependencies that include statically linked binaries
  5. HTML提供的5种空格实体(nbsp`;`ensp`; `emsp`;` thinsp`; `zwnj`;`zwj`;)
  6. 教你如何上传代码到GitHub
  7. java连不上sqlserver_java和SQL连接不上——解决步骤
  8. First love
  9. Openssl移植到ARM开发板
  10. 计算机意识状态在线场所,科学网—浅谈计算机意识的有效范围 - 于金辉的博文...