编译实际就是翻译,是将人类易读(为啥?因为开发语言的目的就是为了让人容易使用)的语言转换为机器或程序易读的语言。Java的编译器是javac,它将.java文件编译为.class文件,也就字节码文件。

和中级语言如C不同的是,Java没有直接生成CPU可读的机器码。为了实现跨平台能力,javac生成的字节码会由不同平台的虚拟机来识别。编译的过程大学本科会学到,在课程《编译原理》中;硕士阶段会继续深入学习形式语言和自动机理论。编译原理的课件网上一大堆,这里不说了。

放一张Java编译过程的图(出处见水印,侵删):

这个只是从编译原理角度看到的Java编译过程。几乎全部语言都是这样的编译过程,但是具体到某一门语言,都会有各自根据语言特性的差异实现。比如Java有注解处理程序,最常见的就是lombok。

上图是OpenJDK官方给出的编译概图。可以看到分为三个阶段:

  1. javac会把命令指定的源文件解析为语法树,并把外部可见的定义放到符号表中
  2. 调用注解处理程序,如果生成了新的文件就重新走第一步,一直到没有新文件产生
  3. 把语法树解析为类文件。类引入的其他依赖会在类路径寻找并被编译

这三个阶段是由JavaCompiler类控制的,我们来分别看一下这三个过程。

com.sun.tools.javac.main.JavaCompiler类,位于tools.jar中。tools.jar提供了Javac/javap/javadoc等命令的实现

解析和进入

解析

源文件会被处理为unicode字符(因为源文件的字符是任意的,可以是uft-8也可以是gbk,也可以是其他任意),在进入虚拟机以前使用的是优化过的utf-8字符,进入虚拟机后是utf-16。然后通过com.sun.tools.javac.parser.Scanner类转换为token。com.sun.tools.javac.parser.JavacParser会读取token流,并调用com.sun.tools.javac.tree.TreeMaker创建语法树。语法树是从com.sun.tools.javac.tree.JCTree的子类构造的,JCTree是一个抽象类,并且是接口Tree的实现类。

进入

每一棵语法树都会被com.sun.tools.javac.comp.Enter处理,它会将遇到的符号都放入符号表。符号表是编译过程中用到的一张hash表,它会记录变量、方法、自定义类等的定义信息,可用于检查代码是否正确(比如变量是否重复定义,因为重复定义会在符号表中冲突),使用符号的时候就直接从符号表拿,并按照符号表中记录的定义使用。语法树生成阶段的产出是一个TODO list,里面是需要分析并生成类文件的语法树。创建语法树包括3个阶段,类排队进入下一个阶段。

在第一阶段,所有类符号都被放入各自的可访问范围(public/private/包/子类),并递归判断引用类的可访问性。完成的时候会给一个com.sun.tools.javac.comp.MemberEnter对象。

如果存在package-info.java,它的语法树也会放进TODO list

在第二阶段,通过调用MemberEnter.complete()方法标记类被处理完。完成包括两步:(1)可以看到类的参数、子类、接口等信息;(2)进入自己的可访问范围。(2)依赖(1),所以(1)后面进入了一个halfcompleted队列。

第三阶段是在符号表都生成以后,符号的注解都会被验证是否合法(比如@Override)。

第一阶段决定了一个定义能不能被访问到,第二阶段是Lazy的,类的成员第一次被访问才会进入符号表,但是最终队列里的类都会被处理掉变成完成。这是由MemberEnter的两阶段来保证的。

注解处理

这里用到com.sun.tools.javac.processing.JavacProcessingEnvironment。

从概念上看,注解处理是编译前的一个小步骤,主要就是解析源文件,看一下需要调用哪个注解处理器。第一轮之后如果产生了新文件,就多执行几轮。直到没有新文件了就开始编译。

而实际上,在文件被编译以前,可能不知道该用哪个注解处理器。所以为了避免使用注解处理器前进行不必要的解析并放进符号表,JavacProcessingEnvironment有点没按照概念模型走,但是依然满足在实际编译前就处理了注解。怎么办的呢?

在文件被解析并进入符号表后,JavacProcessingEnvironment会被调用。一般来说,如果符号有误就会中止处理并报错,但是有可能符号定义是通过注解产生的(比如lombok的getter/setter/ToString),这时候不能报错。

所以要处理注解,要使用单独的类加载器。

注解处理器运行后,JavacProcessingEnvironment会决定是否需要进一步处理。需要继续处理注解的话会创建一个新的JavaCompiler对象,从头开始处理新文件。如此反复。最后JavacProcessingEnvironment会返回一个JavaCompiler对象提醒开始编译,这个JavaCompiler对象要么是最初用于解析源文件那个,要么是最后一轮注解处理中用到那个。

分析和生成

这一步是在前面的基础上生成.class文件的。

分析语法树的时候如果发现依赖了但是没在javac命令中指定的类,就去源文件路径和类路径下查找。如果在类路径下找到,就直接拿过来看看是否是合法使用;如果在源文件路径下找到,就要进行解析、放入符号表、进入TODO list。

语法树的分析和类文件生成是由一些处理TODO list条目的“观察者”完成的。这里并不要求观察者们一个一个的去处理,因为是在内存中完全不必要。只要最终每个条目都被处理即可(除非出错了没法处理)处理过程会用到这些类:

  • com.sun.tools.javac.comp.Attr
  • com.sun.tools.javac.comp.Flow
  • com.sun.tools.javac.comp.TransTypes
  • com.sun.tools.javac.comp.Lower
  • com.sun.tools.javac.jvm.Gen

一旦类文件生成,前面生成的东西就不用了。为了节省内存空间会把他们的引用改成null,以让GC去回收掉。

JAVA文件的编译的更多相关文章

  1. 在windows下使用cmd命令行对java文件进行编译和执行

    windows下利用cmd命令行可以调用jdk里的javac.exe和java.exe对java文件进行编译和执行,前提是jdk已成功安装并正确配置相关环境变量 相关配置链接:java基础学习总结—— ...

  2. Java文件手动编译执行步骤

    Java编译执行步骤: 1)将 Java 代码编写到扩展名为 .java 的文件中.2)通过 javac 命令对该 java 文件进行编译.3)通过 java 命令对生成的 class 文件进行运行. ...

  3. MyEclipse2014,java文件无法编译,run as上是none applicable,不是文件本身的问题

    1.配置一下JDK目录 2.window -> Preferences -> java -> Installed JREs -> Add

  4. javaweb项目部署到tomcat之后java文件没有编译

    1.选中你的项目==>选择Project 2.将Build Automatcally前的对号去掉后再Clean一下你的项目 这样就可以了,

  5. 多个java文件编译并打成jar包经典方法

    首先,多个java文件的编译 find . -type f -name *.java > compilelist (.代表当前路径) javac -cp "$CLASSPATH&quo ...

  6. JAVA 文件编译执行与虚拟机(JVM)简单介绍

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytpo3 java程序的内存分配 JAVA 文件编译执行与虚拟机(JVM)介绍 ...

  7. 在dos中编译java文件

    首先Dos中 编译java文件是:javac (所有)类名.java 运行java文件是:java 包名.类名 java指令默认在寻找class文件的地址是通过CLASSPATH环境变量中指定的目录中 ...

  8. jdk编译java文件时出现:编码GBK的不可映射字符

    出现此问题的几种解决办法: 1.cmd下使用javac编译java文件 如: javac test.java 解决办法:编译时加上encoding选项 javac -encoding UTF-8 te ...

  9. 【转】 Apk文件及其编译过程

    Apk文件概述 Android系统中的应用程序安装包都是以apk为后缀名,其实apk是Android Package的缩写,即android安装包. 注:apk包文件其实就是标准的zip文件,可以直接 ...

  10. Linux巩固记录(2) java项目的编译和执行

    由于要近期使用hadoop等进行相关任务执行,操作linux时候就多了 以前只在linux上配置J2EE项目执行环境,无非配置下jdk,部署tomcat,再通过docker或者jenkins自动部署上 ...

随机推荐

  1. ansible(12)--ansible的cron模块

    1. cron模块 功能:管理被控端计划任务: 主要参数如下: 参数 说明 name 定时任务基本描述 job 定时任务要执行的命令 minute 分 hour 小时 day 日 month 月 we ...

  2. 简易版跳板机-teleport使用

    目录 1 环境搭建 2 teleport工具搭建 3 teleport使用示例 3.1 资产管理-添加主机 3.2 资产管理-添加账号 3.3 创建用户 3.4 运维授权 3.5 安装客户端助手 3. ...

  3. 最小生成树Kruskal算法的实现原理

    到底什么是最小生成树 最小生成树算法应用范围比较广,例如在网络的铺设当中难免会出现环路,需要要生成树算法来取出网络中的环,防止网络风暴的发生.那到底什么是最小生成树呢?我这里就不给严谨的定义了,这种定 ...

  4. 什么是Token?为什么大模型要计算Token数

    本文分享自华为云社区<[技术分享]什么是Token?为什么GPT基于Token定价>,作者:开天aPaaS小助手Tracy. 在使用LLM大模型时,我们经常会遇到一个关键词,叫:Token ...

  5. 鸿蒙HarmonyOS实战-Web组件(Cookie及数据存储)

    前言 Cookie是一种存储在用户计算机上的小文本文件,用于在用户访问网站时存储和提取信息.它由网站服务器发送到用户的浏览器,并存储在用户的计算机上.每当用户访问该网站时,浏览器将发送该Cookie回 ...

  6. MySQL学习笔记-数据定义语言

    SQL-数据定义语言(DDL) 一.操作数据库 1.查询 # 查询所有数据库 show databases; # 查询当前数据库 select database(); 2.创建 create data ...

  7. JavaSE print printf println 区别

    *print与println,printf区别 System.out.print();括号内必须含有参数 System.out.println();括号内可以不含参数,此时代表newline即换行; ...

  8. php分组查询和聚合函数 数据显示的顺序

      // 分组查询和聚合函数         // 分组查询 : 将指定字段中的数据,按照不同的具体数值,进行分组         //           数据相同的分在一个分组中          ...

  9. 使用 Filebeat+Easysearch+Console 打造日志管理平台

    近年来,日志管理平台越来越流行.使用日志管理平台可以实时地.统一地.方便地管理和查看日志,挖掘日志数据价值,驱动运维.运营,提升服务管理效率. 方案架构 Beats 是轻量级采集器,包括 Filebe ...

  10. LNMP集群架构

    网站集群拆分 上一节我们是部署了单机的LNMP,再往下,要进行拆分了,无论是性能.还是安全性,都务必要拆分. 拆分的内容有 nginx集群 mysql nfs共享存储 等 拆分思路 情况1 当前的单机 ...