javac命令的使用和运作原理

王二北原创,转载请标明出处:来自王二北

一、简单唠唠JAVAC

javac这个命令,搞java的都不陌生,很多人在第一次用java输出hello world时,都接触过这个命令,它的作用简单来说,就是将指定的java源代码编译成class文件,后来我们有了Eclipse等IDE工具,就很少关心这个命令了,偶尔需要的时候,才想起来javac xx.java一下,其实我也一样。我也认为javac命令很简单,不就是编译个class文件吗。但是随着对jvm的了解,我才觉得,jdk中提供的很多命令都是很有意思的。javac这个命令也远没有我想想的那么简单。

比如,我在介绍javap命令时,如果对直接使用javac编译的class文件,进行javap xxx查看其反汇编信息时,看不到方法的局部变量表、代码行与指令行的偏移映射表等信息。再比如,jdk8之前,并没有提供通过反射获得方法中入参的参数名的api(如何方法入参参数名,可以参考我的文章)等等。

接下来,我就根据我查到的资料和感悟来简单的说说javac命令。

二、JAVAC命令的工作过程

我们都知道javac命令的作用是将java源码编译成二进制字节码class文件,那么从java源文件编译成class文件这个过程中JAVAC命令都进行了什么操作呢,或者说JAVAC命令的工作过程是什么样的呢?
首先,来看看编译原理(上学时学过,现在基本都还给老师了,网上查了查资料)中编译过程主要经历以下阶段:


javac命令的使用和运作原理_第1张图片
编译过程,图片来自网络

javac命令在进行编译操作时,也会按照类似的过程进行:

(1)词法分析阶段

词法分析,就是将获得的java源代码信息转化为标记(Token)集合,比如关键字、变量、运算符等等,都是一个个的标记。词法分析的过程就是将这些标记解析出来。

(2)语法分析阶段

语法分析是在词法分析得到的标记集合的基础上,抽象出对应的语法树。什么是语法树呢?简单的来说,一个java源文件中包信息,import信息、类定义信息、方法信息、字段信息等待作为一个个的项,这些项集合在一起就抽象为一棵语法树。
为了更直观的解释什么是语法树,这里做个测试,首先在eclipse中创建一个Test1.java文件,在这个java文件中添加两个类Test1和Test2,内容如下:

package com.test.map;
import java.util.Date;

public class Test1 {
   private String name;
   public static final int age = 20;
    
   public void test(String username){
       this.name = username;
       System.out.println(new Date());
   }
    
   public void test2(int a){
       try {
           System.out.println(a/0);
       } catch (Exception e) {
           throw new RuntimeException(e);
       }
   }
}

class Test2{
   public void tst(){
        
   }
}

然后使用EClipse的AST插件(AST插件安装)分析当前java源码的语法树,内容如下,一目了然,很清晰,包含了这个java文件package信息、import引入的依赖信息,定义的类,类的字段和方法等等信息:

javac命令的使用和运作原理_第2张图片

javac命令的使用和运作原理_第3张图片

(3)符号表填充阶段

符号表(SymbolTable)是由一组符号地址和符号信息构成的表格,可以把它想象成哈希表中K-V值对的形式(实际上符号表不一定是哈希表实现,可以是有序符号表、树状符号表、栈结构符号表等)。符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查(如检类型是否匹配等)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

(4)注解处理阶段

在jdk1.5之后,java提供了对注解的支持,注解可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。如果有需要在编译期间被处理的注解,则这些注解将会在当前阶段进行读取和处理。

(5)语义分析

在语法分析之后,编译器获得了程序源码的抽象语法树,语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查。举个例子,假设有如下的3个变量定义语句:

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

后续的代码中可能出现操作赋值运行:

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

上面的代码中,它们都能构成结构正确的语法树,但是只有第1种的写法在语义上是没有问题的,能够通过编译,其余两种在Java语言中是不合逻辑的,无法编译(在java中int类型可以和char、short、byte类型进行加减等操作,但不能和boolean进行相关操作)。
javac的编译过程中,语义分析过程分为标注检查、数据及控制流分析、解除语法糖3个步骤。

1、标注检查

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

int a = 1+2;

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

2、数据及控制流分析

数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。编译时期的数据及控制流分析与类加载时的数据及控制流分析的目的基本上是一致的,但校验范围有所区别,有一些校验项只有在编译期或运行期才能进行。

3、解除语法糖

语法糖,简单的来说,就是在开发语言中添加某些语法,这些语法对开发人员是非常友好和有用的,主要用来使用的开发语义更易用、开发人员开发出的代码有更好的可读性、减少程序代码出错率等。但是这些语法对开发语言的功能和性能并没有太大的影响。
举个例子,java中基本类型和其对应引用类型直接的拆箱装箱操作,在我们的代码中我们可以这样写:

int a = 1;
Integer b = a+2;

实际上,在编译之后,通过gui等class反编译工具,可以看到:

int a = 1;
Integer b = Integer.valueOf(a+2);

在java中,还提供了for(i:xx)循环遍历、支持string的switch case、泛型、变长参数等等语法糖。关于java中的语法糖,后续会出一篇博客,专门讲述。
解除语法糖阶段,就是将我们代码中java提供的语法糖解析还原为java原本的基础语法结构。因为在运行期间,jvm是不支持这些语法糖对应的语法的。

(6)字节码生成阶段

字节码生成阶段,会将前面生成的语法树、符号表等信息转化为字节码输出到磁盘中,并且会进行相关的代码添加和转换工作。
比如,当我们使用javap查看反汇编代码时,会看到通过new创建对象时,实际上是调用了这个对象的方法,完成对象的初始化,这个方法就是在字节码生成阶段添加的,它会将我们在代码中写的普通语句块、成员变量初始化、调用父类构造器等等操作都放入到方法中,完成对象的初始化操作。
再比如,多个字符串变量相加a+b+c,实际上是创建了一个StringBuilder对象,对这些字符串变量进行append()操作,这些通过javap都能看到。
关于javap的使用,可以参考我的博客《通过javap命令分析java汇编指令》。

以上,就是javac命令工作的过程,下面讲述一下javac命令的使用。

三、JAVAC命令的使用

一般安装好jdk后,我们会在控制台执行javac命令,以验证jdk是否安装成功,比如,我在我本地执行javac,会输出一下内容:

用法: javac  
其中, 可能的选项包括:
  -g                         生成所有调试信息
  -g:none                    不生成任何调试信息
  -g:{lines,vars,source}     只生成某些调试信息
  -nowarn                    不生成任何警告
  -verbose                   输出有关编译器正在执行的操作的消息
  -deprecation               输出使用已过时的 API 的源位置
  -classpath <路径>            指定查找用户类文件和注释处理程序的位置
  -cp <路径>                   指定查找用户类文件和注释处理程序的位置
  -sourcepath <路径>           指定查找输入源文件的位置
  -bootclasspath <路径>        覆盖引导类文件的位置
  -extdirs <目录>              覆盖所安装扩展的位置
  -endorseddirs <目录>         覆盖签名的标准路径的位置
  -proc:{none,only}          控制是否执行注释处理和/或编译。
  -processor [,,...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
  -processorpath <路径>        指定查找注释处理程序的位置
  -d <目录>                    指定放置生成的类文件的位置
  -s <目录>                    指定放置生成的源文件的位置
  -implicit:{none,class}     指定是否为隐式引用文件生成类文件
  -encoding <编码>             指定源文件使用的字符编码
  -source <发行版>              提供与指定发行版的源兼容性
  -target <发行版>              生成特定 VM 版本的类文件
  -version                   版本信息
  -help                      输出标准选项的提要
  -A关键字[=值]                  传递给注释处理程序的选项
  -X                         输出非标准选项的提要
  -J<标记>                     直接将 <标记> 传递给运行时系统
  -Werror                    出现警告时终止编译
  @<文件名>                     从文件读取选项和文件名

直接执行javac命令,就等同执行javac -help,它会输出javac这个命令的使用规则,以及可以使用的相关参数及参数的简介。

3.1、JAVAC命令的使用格式

在前面执行javac输出的信息中,我们看到其输出的用法为:

javac

其中:

  • options表示我们在使用javac命令时需要指定的参数选项,可以同时指定多个参数,每个参数之间使用空格隔开。参数选项在javac的使用说明中都列出来了,下一小节,会着重讲解其中的几个参数。

  • source files表示我们要编译的java源码文件。可以是多个文件,使用空格隔开。并且,这些文件文件名都必须以.java结尾。

比如:

javac -nowarn,-verbose Test.java Test2.java

另外,javac后面的参数、文件等信息并没有固定的顺序,你可以按照自己的意愿随便指定各个参数和文件信息的位置顺序,比如:

javac -nowarn Test1.java -verbose

还有一点需要注意:

在前面javac输出的信息中,最后一项:

@<文件名> 从文件读取选项和文件名

意思是,你可以将参数选项,文件信息单独放到一个或多个文件中,然后执行:
javac @xxx文件就可以在执行javac命令时,将xxx文件中的内容传递给javac命令。

例如,有两个java文件Test1.java,Test2.java。然后新建一个classmsgs.txt文件,文件内容为:

Test1.java Test2.java -verbose

然后执行下面的命令:

javac -nowarn @classmsg.txt

如果有多个文件,可以使用空格隔开,如:

javac -nowarn @classmsgs.txt @classmsgs2.txt

就可以看到,Test1和Test2被编译成class文件,并且在编译时会输出编译器正在执行的操作日志(因为使用了-verbose参数)。

一般情况下,当你需要编译的java文件比较多,或者需要设置的参数比较多时,亦或者要复用一些参数信息时,可以将这些java文件名或者参数项抽取出来,放到一个或多个文件中。这一点很像spring中xml的import,css中@等在一个文件中引入其他文件的方式。看来软件开发中,很多地方都是想通的。

综上,javac的实际使用格式可以归纳为:

javac @files

3.2、JAVAC命令中的参数项

在前面直接指向javac命令输出的信息中,我们看到javac用到的参数项有很多,那么这些参数项都是用来干什么的得呢?这里我按照自己查阅的资料以及自己的实验和理解,做一下讲解。这些参数项很多,我将它们分为一下几类:
先写到这里,先挖个坑,后续再填。

你可能感兴趣的:(javac命令的使用和运作原理)