Java语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(叫“编译器的前段”更准确)——把*.java文件转变成*.class文件的过程;

也可能是虚拟机的后端运行期编译器(JIT)把字节码转变成机器码的过程;还可能是指静态提前编译器(AOT编译器)直接把*.java文件编译成本地机器码的过程。

Javac编译器

Javac编译器不像HotSpot虚拟机那样使用C++语言实现,它本身就是一个又java语言编写的程序。java虚拟机并没有对如何把Java源码文件转变为Class文件

的编译过程进行十分严格的定义,这导致Class编译在某种程度上与具体JDK实现相关。从Sun javac来看,编译过程大致可以分为3个过程,分别是:

- 解析与填充过程

- 插入式注解处理i的注解处理过程

- 分析与字节码生成过程

Javac的编译过程

解析与填充过程

解析步骤包括了经典程序编译原理中的词法分析和语法分析两个过程

1、词法、语法分析

词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素。关键字、变量名、字面量、运算符

都可以成为标记。如“int a = b + 2” 这句代码包含了6个标记。在Javac源码中,词法分析过程由com.sun.tools.javac.parser.Scanner类来实现。

词法分析是根据Token序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点

都代表着程序中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至代码注释都可以是一个语法结构。

在Javac源码中,语法分析过程由com.sun.tools.javac.parser.Parser类实现,这个阶段产出的抽象语法树由com.sun,tools.javac.tree.JCTree类表示,经过这个步骤之后

编译器基本不会对源码文件进行操作了,后续操作都建立在抽象语法树之上。

2、填充符号表

符号表,是由一组符号地址和符号信息构成的表格,符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查和产生中间

代码。在目标代码生成阶段,对符号名进行地址分配时,符号表是地址分配的依据。在javac源代码中,填充符号表的过程有com.sun.tools.javac.com.Enter类实现,此过程的

出口就是一个待处理列表,包含了每一个编译单元的抽象语法树的顶级节点,以及package-info.java的顶级节点

注解处理器

注解与普通的Java代码一样,是在运行期间发挥作用的。插入式注解处理器在编译期间对注解进行处理,我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取,

修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器就将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有

再对语法树进行修改为止,每一次循环称为一个Round。

语义分析与字节码生成

语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是对结构上

正确的源程序进行上下文有关性质的审查。

1、标注检查

Javac的编译过程中,语义分析过程分为标注检查以及数据及控制流分析两个步骤。标注检查步骤检查的内容包括诸如变量使用前会否已经被声明、变量与复制之间的数据

类型是否能够匹配等。在标注检查步骤中,还有一个重要的动作称为常量折叠,如果我们在代码中写了如下定义:

int a = 1 + 2;

那么在语法树上仍然能看到字面量 “1”、“2”以及操作符“+”,但是在经过常量折叠之后,它们将会被折叠为字面量“3”。如图:

由于编译期间进行了常量折叠,所以代码里面定义“a=1+2”比起直接定义“a=3”,不会增加程序运行期哪怕一个CPU指令运算符。

2、数据及控制流分析

数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有复制、方法的每条路径是否都有返回值、是否所有的受检查

异常都被正确处理等问题。编译器的数据及控制流分析与类加载时的数据及控制流分析的目的基本上是一致的,但校验范围有所区别,有一些校验项只有在编译器或运行

期才能进行。

//方法一带有final修饰

public void foo(final intarg) {final int var = 0;//do something

}//方法二没有final修饰

public void foo(intarg) {int var=0;//do something

}

第一个方法的参数和局部变量定义使用了final修饰符,而第二种方法则没有,在代码编写时程序肯定会受到final修饰符的影响,不能再改变arg和var变量的值,但是这两段代码

编译出来的Class文件是没有任何一点区别的。局部变量与字段是有区别的,它在常量池中没有CONSTANT_Fieldref_info的符号引用,自然没有访问标志(Access_Flags)的

信息,甚至连名称都不会保留下来(取决于编译时的选项),自然在class文件中不可能知道一个局部变量是不是声明为final了,因此,将局部变量声明为final,对运行期是没有

什么影响的,变量的不变性仅仅由编译器在编译期间保障。

3、语法糖

语法糖,也称为糖衣语法,指在计算机语言中添加某种语法,这种语法对语言功能并没有影响,但是更方便程序员使用。通常来说,使用语法糖能够增加成的可读性,从

而减少程序代码出错的机会。Java中最常用的语法糖主要是泛型、变长参数、自动装箱、拆箱等。虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,

这个过程称为解语法糖。

4、字节码生成

字节码生成是Javac编译过程的最后阶段,字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的

代码添加和转换工作。

例如,实例构造器()方法和类构造器()方法就是在这个阶段添加到语法树之中的(注意,这里的实例构造器并不是指默认构造函数,如果用户代码中没有

任何构造函数,那编译器将会添加一个没有参数的、访问行与当前类一直的默认构造函数,这个工作在填充符号表阶段就已经完成),这两个构造函数的产生过程实际上是一个

代码收敛的过程,编译器会把语句块、变量初始化、调用父类的实例构造器(仅仅是实例构造器,()方法中无需调用父类的(),虚拟机会自动保证父类构造器的执行

但在()方法中经常会生成调用java.lang.Object的()方法的代码)等操作收敛到()和到方法之中,并且保证一定是按照先执行父类的实例构造器,然后初始化

变量,最后执行语句块的顺序进行。

Java语法糖的味道

几乎每种语言或多或少都提供过一些语法糖来方便程序员的代码开发,这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高效率,或能提升语法的严谨性,或能减少编码

出错的机会。它可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能使效率提升,但是我们也应该去了解这些“小把戏”的真实世界。

泛型与类擦除

泛型的本质是参数化类型的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

Java语言中的泛型它只在源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型了,并且在相应的地方插入了强制类型代码,因此,对于运行期的Java语言来说

ArrayList与ArrayList就是同一个类,所以泛型技术实际上是java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

可以通过Signature、LocalVariableTable等新的属性用于解决伴随泛型而来的参数的识别问题,Signature是其中最重要的一个属性,它的作用就是存储一个方法在字节码层面的

特性签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型信息。Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的Code属性

中的字节码进行了擦除,实际上元数据还是保留了泛型信息,这也是我们能通过反射手段获得参数化类型的根本依据。

自动装箱、拆箱与遍历循环

自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法。如Integer.valueOf()与Integer.intValue()方法。而边儿循环则把代码还原成了迭代器的实现,这也是为何遍历循环

需要被遍历的类实现Iterator接口的原因。最后看变长参数,它在调用的时候变成了一个数组类型的参数,在变长参数出现之前,就是使用数组来完成类似功能。

注意:包装类的“==”运算在不遇到算数运算的情况下不会自动拆箱,以及它们equals()方法不处理数据转型的关系。

条件编译

许多程序设计语言都提供了条件编译的途径,如C、C++中使用预处理器指示符(#ifdef)来完成条件编译。C、C++的预处理器最初的任务是解决编译时的代码依赖关系,而在

语言中并没有使用预处理器,因为java语言天然的编译方式(编译器并非一个个地编译Java文件,而是将所有编译单元的语法树顶级节点输入到待处理列表后再进行编译,因此各个

文件之间能够相互提供符号信息)。无须使用预处理器。那java语言是否有办法实现条件编译呢?当然可以,方法就是使用条件为常量的if语句。如下代码:

public static voidmain(String[] args) {if(true) {

System.out.println("block1");

}else{

System.out.println("block2");

}

}

上述代码编译后Class文件的反编译结构:

public static voidmain(String[] args) {

System.out.println("block1");

}

只能使用条件为常量的if语句才能达到上述效果。如果使用常量与其他带有条件判断能力的语句搭配,则可能在控制流分析中提示错误,被拒绝编译。

Java语言中的条件编译的实现也是java语言的一颗语法糖,Java语言还有不少其他的语法糖,如内部类、枚举、断言、try语句中定义和关闭资源等。

解释器与编译器

Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认为“热点代码”。为了提高热点代码的执行效率,

在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行优化完成这个任务的编译器称为即使编译器。

解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器发挥

作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。

HotSpot虚拟机中内置了两个即时编译器,分别成为Client Compiler和Server Compiler,简称为C1编译器和C2编译器。目前主流的HotSpot虚拟机,默认采用解释器与其中的

一个编译器直接配合的方式。程序使用哪个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户可以使用“-client”或

“-server”参数去强制指定虚拟机运行在Client模式或Server模式。

由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码所花费时间可能较长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集

性能监控信息,这对解释执行的速度有影响。为了在程序启动相应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启用分层编译的策略。

编译对象与触发条件

在运行过程中会被即时编译的“热点代码”有两类,即:

- 被多次调用的方法

- 被多次执行的循环体

对于第一种情况,由于是由方法调用触发的编译,因此编译器理所当然地会以整个方法作为编译对象,这种编译也是虚拟机中标准的JIT编译方式。而后一种情况,尽管

编译动作是循环体所触发的,但编译器依然以这个方法作为编译对象。这种编译方式因为编译发生在方法执行过程之中,因此形象地称之为栈上替换。

编译优化技术

Java程序员有一个共识,以编译方式执行本地代码比解释方式更快,之所以有这样的共识,除去虚拟机解释执行字节码时额外消耗时间的原因外,还有一个重要原因就是

虚拟机设计团队把对代码的所有优化措施都集中在了即时编译器之中了。因此编译器产生的本地代码会比Javac产生的字节码更加优秀。

java源码编译_java 源码编译相关推荐

  1. java 及时编译_Java 面试-即时编译( JIT )

    当我们在写代码时,一个方法内部的行数自然是越少越好,这样逻辑清晰.方便阅读,其实好处远不止如此,通过即时编译,甚至可以提高执行时的性能,今天就让我们好好来了解一下其中的原理. 简介 当 JVM 的初始 ...

  2. java string 异或_Java源码——String

    最近在研究java的源代码,但是由于自己英语水平有限,所以想使用中文注释的方式把源码里的方法全部重写 一遍,下面是楼主整理出来的一小部分.我把整体的项目托管到GitHub上了,欢迎大家前去交流学习. ...

  3. java装逼的话_Java 源码装逼技能之让人懵逼的符号

    源码就是符号位 + 二级制数值.符号位是第一位,0 表示正数,1 表示负数. Java 中 byte 类型一字节八位,可以表示 [1111 1111 , 0111 1111],取值 [-127,127 ...

  4. java代码管理工具_java源码管理与版本控制工具,图文详解

    近些年社会科学技术水平的发展越来越快速了,这也促进了人们对于新技术新知识的学习.尤其是java的应用也越来越广泛.今天就来为大家介绍一下,java源码管理与版本控制工具,一起来了解一下吧. java项 ...

  5. java的resize函数_Java源码解析HashMap的resize函数

    hashmap的resize函数,用于对hashmap初始化或者扩容. 首先看一下该函数的注释,如下图.从注释中可以看到,该函数的作用是初始化或者使table的size翻倍.如果table是null, ...

  6. java计算本金利息_Java源码——复利的计算(compound interest)

    代码功能: 给出本金,计算在不同的年复合利率下不同经过年数(期数)对应的本息和. 代码: package v1ch03.CompoundInterest; /** * This program sho ...

  7. java 字节码分析_Java 字节码实践 - 解读

    最近刚看完 深入理解 Java 虚拟机 一书中的第 6 章 (类文件结构),便迫不及待地自己写一个小的 Demo,来自己分析一把 Java 源文件经过编译之后成为字节码文件到底是个什么东西?先由一个简 ...

  8. java程序编译_Java程序的编译过程

    Java的编译期是一个模糊的概念,需要具体分析. 将 *.java文件转为 *.class的过程称为编译器的前端(前端编译).例如:JDK的javac编译器. 把字节码( *.class文件) 转变为 ...

  9. java反码补码原码作用_java原码补码反码关系解析

    本文为大家解析了java原码补码反码的关系,供大家参考,具体内容如下 原码:不管源数据是十进制还是十六进制,统统将数字转成二进制形式 反码:把原码的二进制统统反过来,0变成1,1变成0 补码:负数的反 ...

  10. java无法编译_Java静态方法无法编译

    当我编译这段代码时,会出现以下错误. 中的提取字符(java.郎,字符串,int) 问题2不能应用于(). 我应该修复什么? 谢谢. import java.util.Scanner; public ...

最新文章

  1. 编译器设计-自下而上分析器-误差恢复-语义分析
  2. hdu - 4707 - Pet
  3. Matlab绘制包含双Y轴的图
  4. 160个Crackme003之4C大法详解
  5. mysql 先排序再去重_有人说先学会三轴,再去搞四轴、五轴加工中心,这几种有何区别?...
  6. AdvFlow:一种基于标准化流的黑盒攻击新方法,产生更难被发觉的对抗样本 | NeurIPS‘20
  7. 网页证书添加_二、Exchange2016部署及基础配置(NDS及证书配置)
  8. 6 PP配置-生产主数据-工作中心相关-工作中心标准值参数
  9. html音乐播放器代码自动,html5 css3音乐播放器代码
  10. 设计模式的C语言应用-观察者模式-第四章
  11. element ui select 自动向上向下弹出_达观数据:Selenium使用技巧与机器人流程自动化实战...
  12. typora markdown 标题自动编号
  13. 人工智能白皮书(2022年) 附下载
  14. Tomcat 发布时war解压
  15. 微信小程序——小程序UI框架首页
  16. 计算机控制系统——导论
  17. 盘点各大互联网公司2017中秋月饼设计,你最喜欢哪一个?
  18. module 'gensim' has no attribute 'corpora'
  19. OSI七层的基础概念
  20. Python基础之函数,面向对象

热门文章

  1. 服务器获取真实客户端 IP [ X-Forwarded-For ]
  2. 001_JavaScript数组常用方法总结及使用案例
  3. Django-组件拾遗
  4. jQuery als.js 跑马灯
  5. 鉴客 C# 抓取页面(带认证)
  6. 水印代码WPF 实例下载
  7. 项目经理的三个立足点
  8. 3. 什么是icmp?icmp与ip的关系_0.3亿人口的美国会比3亿人口的美国富裕吗?
  9. dubbo源码之SPI机制源码
  10. 并发编程之ReadWriteLock接口