java 源码 编译 执行-JAVA代码编译过程

前言

写了这么多年代码,你清楚java代码运行的整个流程吗?

你会不会和我一开始一样,以为点击IDE中的RUN按钮就可以直接运行我们写的代码了?

老话说得好,你觉得日子过得好,虽然只是因为有人替你背负了重担,但编译器和虚拟机却默默地承担了这一切。

小小的RUN背后是很多组件共同努力的结果。 他们必须非常努力地工作才能看起来毫不费力。

第一个问题来了,计算机真的能听到我们写的“诗”吗?

众所周知,Java是一种“一次编译,随处运行”的语言,也就是所谓的平台无关性。 无论在哪个平台都可以运行,并且保证运行结果与预期一致。 (这是大学老师反复指出的)

Java实现“平台无关”的原理也很简单,就是用一种中间格式来进行过渡,也就是我们常说的字节码。 通过将Java源代码转换为字节码,它保证了JVM(Java虚拟机)读取到的一定是你仍然可以识别的字节码格式。

简单解释一下:你不会说英语,西班牙人也不会说英语,大家或多或少都会说日语,用法语作为大家的中间格式,保证双方都能明白对方的意思。 这就是所谓的跨平台。

Java源代码首先被编译成字节码,而这个字节码是实现平台独立性的关键。 无论你在什么类型的平台上,只要你安装了一个可以识别字节码的JVM(Java虚拟机),通过JVM通过解析字节码文件并将字节码转换为特定平台上的机器指令,可以实现平台化运营。

因此,别说让计算机底层读取我们写的“代码诗”,就连Java虚拟机也无法获取我们的原始代码。 在编译器的努力下,Java源代码已经变成了白话类文件。

所以,亲爱的,操作系统无法欣赏我们的“诗意代码”。 我们编写的每一行代码都会变成指令。 对于操作系统来说,它看到的不是编程的艺术,而是它需要的东西。 这只是一系列已经完成的KPI。

文本即代码?

如果我们编写一个内容相同的Java文件和txt文本,它们在文本编辑器中看起来会是一样的。

有一句谚语:世界上最好的 IDE 是 txt 文本编辑器。 现在我们或许可以顺利使用IDE了。 我们习惯让IDE给我们很多操作的提示,依赖IDE的代码补全和快捷键。

但传说中,有一群掠食者,可以用记事本写出漂亮的代码。 当他们达到这种状态时,他们就已经成为一个有代码的人了。 不需要句型突出显示或完成提示。 正确的句型全部清晰。 注意,敲出的每一行代码都是好代码(doge),可以直接编译运行,没有bug。

有点牵强,但是用记事本实现开发功能确实是可以的。 只要你输入的代码逻辑正确,没有语法错误,最终保存的后缀就是.java,可以作为代码运行。

因此,从本质上来说,我们输入的txt文本和一开始的Java代码并没有太大的区别。 我们的带有.java后缀的文件也可以用普通的文本编辑器打开。 而文本编辑器能做的也仅限于收听.java 文件上的代码文本。

Java编译器是终极的,能够识别和理解.java文件的存在。

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

可以说,编译过程是Java开发的第一步,但同时也是程序的一大步。

接下来我们先介绍一下编译器在Java系统中的地位。

JDK和JRE的爱恨情仇

我们刚开始学习Java的时候,肯定已经安装了所谓的Java环境。 当我们满怀信心地进入Oracle的Java官网时,映入眼帘的是两个看起来非常相似的安装包。

我被骗了。 我只是想安装一个Java环境。 怎么有两个奇怪的安装包,一个叫JDK,一个叫JRE。 这两个安装包和什么又叫“Java”有什么关系呢? ?

我们先搞清楚所谓的JDK和JRE的区别,先看一张Java8架构图

JDK的全称是Java Development Kit(Java开发工具包),它包含了Java从开发到运行的各种工具。

JRE是指Java运行时环境(JavaRuntimeEnvironment),它包括基本泛型和JVM虚拟机。

上图展示了Java8的架构。 最右边一栏清楚地显示了JDK和JRE各自的范围,我们可以很容易地找到:

JRE 是 JDK 的子集。

既然要从事开发,就必须保证自己写的代码能够运行,所以当开发者安装JDK的时候,就已经包含了一个运行环境JRE,以保证他的代码能够运行和验证。 这就是 JDK 中包含 JRE 的原因。

但如果我们是普通用户,不关心开发,甚至根本不懂代码,只想要代码运行的结果,那么我们只需要本地的JRE运行环境即可。

反过来想,既然安装JRE可以运行JAVA代码,但是需要完整的JDK才能完成开发,那么它们之间的区别肯定和开发流程有关。

那么我们就把它拿出来,解释一下为什么缺少这块内容只能成为运行环境,而不能成为开发功能呢?

在本节中,我们可以看到几个熟悉的命令:

其中,我们最常用也是最重要的就是javac命令。 这是JDK中嵌入的编译器。 通过该命令可以将java源文件转换为class文件。 这个javac编译器是JRE相比JDK缺乏开发功能的决定性因素! !

我们用一个简单的例子来看看,开发者编译出来的java代码在完整的JDK框架下经历了JDK、JRE和JVM的运行过程。

我们用一个简单的例子来看看,开发者编译出来的java代码在完整的JDK框架下经历了JDK、JRE和JVM的运行过程。

可以看到,通过JDK中的javac命令,我们可以将java源代码编译成class文件。 上面说了,这个class文件是最终在JVM中运行的文件。

我们把从java源代码到class文件的过程称为编译阶段,将class文件运行到JVM中得到结果的阶段称为运行阶段。

因此,如果只有JRE而没有完整的JDK,就相当于缺少了一个编译源代码的重要工具。 你只能依靠别人传过来的已经编译好的类代码来运行程序,而没有能力修改和开发它。 能力。

如果你聪明的话,你很快就会发现,由于虚拟机只需要class文件就可以运行,所以它并不关心顶层使用哪种语言,只要支持生成JVM可以识别的字节码即可。

难道说……

没错,恭喜你发现了 JVM 虚拟机**的“跨语言”特性。

很多语言都依赖这些特性,编译自己的源代码生成class文件,并基于JVM虚拟机运行。 比较常用的是Scala和Kotlin,甚至可以和Java语言互相调用。 由于它们最终都会被编译成类文件并在虚拟机中运行,尽管在源代码阶段它们是不同的语言,但在编译之后,你们最终都会得到相同的字节码。

其实如果再极端一点的话,因为class文件本质上就是一个二进制补码文件,所以只要你足够强大,能手写出你需要的二进制补码文件,就不再需要编译器了(狗头)救生)。

很多读者都想说:“我们是来学技术的,不是学魔法的。”

别笑,直接改字节码并不是天上飞的魔法咒语,而是真正的技术。 和我们熟悉的lombok一样,我们可以根据我们编译的注解来生成字节码,从而实现字节码的改变和改进==(但是lombok也依赖于编译器的一些特性,在编译阶段触发操作)==。

同样,一些字节码改进技术如ASM也是通过直接操作字节码来实现的。

通过字节码增强技术,可以实现热部署等操作java 源码 编译 执行,使更改代码后无需重启服务即可生效; 还可以实现日志注入等功能,并且可以在不改变客户端调用方式的情况下减少或缓存指定的方法。 日志功能。

但对于大多数普通开发者来说,编译器仍然是必不可少的。

编译阶段

当调用javac命令时,会触发java代码的编译过程,将.java文件编译成.class二补代码文件。

那么,在编译器中,源代码是如何一步步变化的呢?

注意:javac是javac编译器的内置命令,但javac并不是市场上唯一可用的编译器。 其他一些厂商也根据Java标准开发了自己的编译器。 例如Eclipse的ecj(Eclipse Compiler for Java)等。

只是大多数人使用的是JDK自带的javac编译器,所以下面的讨论都是基于javac编译器进行的。

可以理解,编译的过程就是“编译”和“翻译”。

编辑器:将Java源代码的结构组织成合适的格式,包括编译过程中的具体句型树和符号表,最后将源代码编码成class文件。

翻译:解析源代码中的语义并将其精确地翻译成另一种方式(字节码)。 这一步既保证了原始格式的正确(Java源代码中的句型是正确的),又保证了翻译后的字节码与源代码中表达的含义一致。

也就是说,编译过程必须保证输入格式符合Java语言规范,输出格式符合Java虚拟机规范。

这个过程听起来很复杂,读者可以回忆一下自己经历过的代码编译失败的场景。 每次编译失败都是编译器默默工作的结果。 在编译过程的不同阶段可能会发现并抛出不同的错误。 出去。

接下来我们一步步告诉大家编译的具体步骤,以及编译过程中各个阶段抛出的不同编译异常。

事情看起来好多了。 总结起来,大致可以分为以下几个步骤:

1. 词法分析&句子分析

词法分析是第一步。 它的主要功能是将源代码的字符流转换为Token集合。 Token是指代码中具有独立语义且不可再分的标记。

这里需要注意的是,Token并不是指单个字符,而是一个具有实际意义的单词。 而且,编译器会识别不同的词法类型,并为其分配相应的Token类型。 例如,int将被识别为Token.INT,运算符也会被分配给相应的Token类型。 例如,+是Token.PLUS:

代码被解析成一系列Token集合后,下一步就是分析句型了。

句型分析就是根据解析出的Token集合,解析出一棵具体的句型树(AbstractSyntaxTree,AST)。 AST 包含 Java 代码中的层次结构。

根据这种结构,代码中的所有变量、方法甚至注释等各种信息都可以分层显示。

构建 AST 的过程会判断 Token 的类型是否与其在树中的位置相匹配。 这一步很容易理解。 当你使用关键字作为变量名时,编译不会通过,就在这一步被捕获。

比如你用这段代码来编译:

public class Hello {
    public static void main(String[] args) {
        String enum = "world";
        System.out.println("Hello world");
    }
}

会报如下错误:

错误:截至发布 5,“enum”是一个词,并且不能用作标识符

由于enum是关键字,所以在构建句子树的时候,发现标识符的位置出现了关键字,这是不可能的!

因此AST树的建立失败,编译时报错。

词法分析&句型分析是源代码中文本的具体表示。 根据编译器的具体规则对.java源代码中的文本结构进行拆分和解析,为后续的编译工作做好铺垫。 之前的操作就无从谈起了。 不要打开此 AST。

2.填写符号

符号表是由符号地址(位置)和符号信息组成的“表”,其中存储了标记对应的类型、范围等。

这里说是“表”可能会给读者造成一些误解。 其实它并不是我们想象中的二维表,而是更接近于哈希表的通配符对结构。 符号表可以由链表或树结构组成。 或者用栈等结构来实现。

该符号表可以在许多后续步骤中使用,例如:

static char x;  
int foo() {     
  int x;     
  {        
    float x;     
  }  
}

这段代码有三个同名的变量。 聪明的读者肯定能够区分出各自的范围,而愚蠢的计算机则无法这么快分辨出来。

为了在解析符号和类型时区分它们的作用域而不引起使用冲突,需要通过符号表记录关系。

填充符号表的过程可以描述为:

将每个AST的top节点放入待处理列表中,一一处理; 将所有类符号(类声明、名称)输出到内部作用域的符号表中; 如果是package-info.java文件(描述整个包的信息以及包内的常量),则将其顶节点放入待处理列表中; 明确类库类型的真实类型; 如果类中没有构造函数,则添加一个默认的 None Parameter 构造函数; 将类中的符号导入到类自己的符号表中。

这一步有点具体。 您无需太担心细节。 您可以了解大致流程和目的。 你只需要明白这一步是生成一个符号表,记录了类中符号的类型、属性等信息。 方便又方便。 在后续工序中的应用。

指出5.学过Java基础的人都知道,如果一个类没有定义构造函数,那么就会默认创建一个不带参数的默认构造函数。 添加默认构造函数的操作也是在填写符号表时完成的。

为什么?

很简单,因为类的构造方法也需要记录在符号表中,但不能为空。 既然你没有指定,我就给你一个默认的空参数构造函数,然后记录在符号表中。

3.标注处理

从JDK5开始,Java就提供了对注解的支持,在程序中使用注解早已是很常见的操作了。

但需要注意的是,并非所有注释都在编译时起作用。 我们平时使用反射处理的注解主要是指运行时注解。 运行时注释在编译时不受影响。 编译后,class文件仍然会被保留,直到class文件从JVM运行时才最终生效。

编译时注解是指使用 @Retention(RetentionPolicy.SOURCE) 定义的注解,并在编译时处理。 这种类型的注释不会保留在类文件中。

听起来很混乱,但毕竟你在编译过程中不经意间多次遇到过这个注解处理步骤。 例如常用的lombok就在这一步起作用。

Lombok 使用编译时注释处理。 因此,当我们编译带有lombok注解的.java文件后,打开生成的class文件,可以看到lombok相关的注解消失了,相应的getter和setter方法也消失了。 被注入到类文件中。

下图显示的不是class文件,而是相当于添加lombok注解的源码。 左右代码生成的字节码是相同的。

在这一步中,lombok的标注处理器生效,改进了我们上面提到的具体句子树AST。

首先找到@Data注解所在类对应的句型树(AST),然后改变句型树(AST),减少getter和setter方法定义的对应树节点,即可实现我们需要的功能。

这一步也是为数不多的步骤之一,编译器给程序员留下了自己编写代码影响源代码编译过程的机会。

标注处理完成后,可能会形成新的符号,因此如果进行标注处理,则需要再次进行符号表的解析和填充操作(返回步骤2)。

4. 语义分析

语义分析听起来和第一步的词法分析&句子分析很相似,但显然有很大不同。

我们用英语类比来解释一下:

敖丙道:“明天我吃你的饭吗?”

词法分析的步骤相当于把这句话拆成you,eat,today,rice,have you,have you,? ,这几个习语。 每个字都很好。

但到了语义分析阶段,我们再根据规则测试这句话的语义,发现这句话似乎不合逻辑。

回到编译过程来解释,语义分析的作用就是从结构和规则上对源代码进行检测,包括声明检测和类型检测等。

这里我们用周志明老师书中的一个反例来说明:

假设有三个句子定义如下:

int a = 1; 
boolean b = false; 
char c= 2; 
int d =a + c; 
int e = b + c; 
char f = a + c; 

只有这段代码才能通过第一步词法分析和句型分析,并形成正确的AST,而在语义分析时就会报错。 因为编译器发现对变量e和f的操作是非法的,操作涉及的两个值的类型与操作符的逻辑不匹配。

语义分析进一步检测上下文中变量的规范性,例如变量是否已经被声明、变量的数据类型是否与其参与的操作相匹配等等。

如果我们要细分语义分析,可以分为以下几个小阶段:

4.1 指示检测

这就是我昨天说的,检查变量是否提前声明以及操作类型是否匹配的步骤,而这一步的处理会影响AST的结构:

注意,如图所示,我们首先需要检测变量a是否被声明(声明检测),并检测a的类型(类型检测)。 这两个测试都需要使用我们之前已经填写的符号表。 从符号表中查询变量的范围和类型,完成语义分析的检测。

然后判断该运算符和另一个运算值的类型,检查左右运算值的类型是否匹配,可以参与运算。

你听到了吗,这里 AST 和符号表一起工作。

据悉,标记检测步骤有两个非常重要的操作:

子类化技术类型的推断:这一步需要明确子类化方法传递的实际类型; 恒定折叠:

这是一个非常有趣的操作。 它会进行一些简单的常数计算,如:inta=1+2; 在这一步中,将优化为a=3。 优化后,AST中仍然可以看到int、a、1、+、2、; 这些标记,并且该表达式的值已被估计并标记在 AST 上。 也就是说,今天的AST不仅保留了表达式的结构,还记录了表达式的结果。

随后在虚拟机中执行字节码时,由于编译时常量折叠的优化,虽然inta=3和inta=1+2的运行效率是一样的,因为这个常量的操作已经执行过了在编译时完成后,运行时不会消耗额外的处理时间。

通常代码优化是在生成字节码之后,然后在运行时在虚拟机的类库中进行的。 常量折叠是javac编译器对源代码所做的极少数优化之一,也是编译过程中为数不多的对代码进行优化的操作之一。

4.2 数据流分析

数据流分析是检测指示后的进一步检查。 主要检查局部变量在使用前是否是确定性的形参,以及声明的返回值是否具有确定性的返回值等。

值得注意的是,最终变量的不可重复形式参数的性质也在这一步被检测到。 如果final变量是重复的形式参数,编译器会注意到并报告错误。 正是因为这个特性,使用final关键字的局部变量只会在编译时进行校准,而不会在运行时产生任何影响。

java 源码 编译 执行-JAVA代码编译过程

有以下例子:

// 方法1
public void aobingTest(final int nezha){
  final int a = 0;
}
// 方法2
public void aobingTest(int nezha){
  int a = 0;
}

==这两种方法形成的字节码是完全一样的,没有任何区别。 ==因此,所有对最终不可重复形参的限制都在编译时进行了检查。 如果声明为final的局部变量重复形参,编译时会报错。 如果最后的重复形参没有错误,则字节码生成成功。

因此,对于运行时来说,无论局部变量是否声明为final,都不会有任何校准步骤(由于局部变量不受final限制,生成的字节码是相同的,不会保留局部变量在字节码中。有关变量是否声明为final 的信息)。

5. 解句糖

简单来说,句型糖是程序员一种便捷的书写方式。 这类句型不会对最终结果产生实际影响,但也可以减少程序员的工作量。

比如java中的手动拆箱装箱函数、foreach循环函数等,都是句子糖封装的,以便程序员可以写出更简洁的代码。

然而,当程序运行时,这样的句子糖对于计算机来说是无法理解的。 因此,有必要在编译阶段对句型糖进行解码,将句型恢复到原来“笨拙”的样子。

例如,将包装类型拆分为普通类型,并将改进的 for 循环替换为普通的 for 循环。

6.生成Class文件

、初始化由{}符号包裹的成员变量和代码块""/>

注意,这两个看起来像init的方法并没有引用类中的构造函数。

第一种方式是类构造函数,其作用是初始化所有静态变量并执行用static{}包裹的代码块,该技巧的集合如下:

将与类相关的初始化代码收集在一起,以便生成函数,这些函数在类加​​载时按顺序运行,因此该方法相当于将静态代码打包在一起,等待后续统一执行。 第二个方法虽然是实例构造函数,但它的作用是初始化类中的成员变量java 源码 编译 执行,比如成员变量的形参操作,以及用{}符号包裹的代码块,这个方法都会收敛到方法中成为技能相关到对象初始化。 这个技巧的收藏也是有序的:

一般来说,这两种方法都是将源代码中的代码块和变量初始化步骤按照静态和非静态分为两类,并按照一定的顺序打包,等待合适的时机执行。 对于方法来说,合适的执行时间是类加载时; 对于方法来说,执行时间是当类的一个对象是new的时候。

由于类加载过程优先于对象实例化过程,因此技术必须在方法之前执行。 为此,它们的完整执行顺序是:

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

你注意到了吗? 这是一个常见笔试问题的标准答案:“java代码的加载顺序”。

这个问题的本质是,Java代码之所以能够保持加载顺序,是因为在生成class文件时,将按顺序拼接的和方法添加到class文件中,然后在class文件中按顺序执行后续的运行过程。

除了生成构造函数之外,在生成类文件时还会对各个代码逻辑实现方法进行优化,例如将字符串的+操作替换为StringBuffer或StringBuilder的append()技术。

至此,从java源代码到class文件的编译过程就结束了。

一些想法

顺便说一句,还有一个问题可能是你的误解。

很多人认为类文件=字节码。 这是错误的。 类文件不等于字节码。 我们可以从类文件的结构中窥见一斑。 类文件中记录了以下信息:

1 结构信息:class文件格式版本号;

2、元数据:主要对应Java源代码中“声明”和“常量”对应的信息,包括类的声明信息、类中属性字段和技能的声明信息、常量池、 ETC。;

3路信息:主要对应Java源代码中“语句”和“表达式”对应的信息,包括字节码、异常处理表、操作数栈和局部变量区的大小等;

现在很清楚,字节码是Class文件的子集,只是Class文件中众多组成部分之一。

好吧,不要再认为 Class 文件是字节码了。

你知道的越多,你不知道的就越多

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 源码编译 java 源码 编译 执行-JAVA代码编译过程 https://www.wkzy.net/game/189861.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务