深入理解Java虚拟机—前端编译与优化

上一篇:深入理解Java虚拟机—类加载及执行子系统的案例与实战

下一篇:深入理解Java虚拟机—后端编译与优化

前端编译与优化

在Java技术下谈“编译期”而没有具体上下文语境的话,其实是一句很含糊的表述,因为它可能是指一个前端编译器(叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程;也可能是指Java虚拟机的即时编译器(常称JIT编译器,Just In Time Compiler)运行期把字节码转变成本地机器码的过程;还可能是指使用静态的提前编译器(常称AOT编译器,Ahead Of Time Compiler)直接把程序编译成与目标机器指令集相关的二进制代码的过程。下面笔者列举了这3类编译过程里一些比较有代表性的编译器产品:

  • 前端编译器:JDK的Javac、Eclipse JDT中的增量式编译器(ECJ)[1]。
  • 即时编译器:HotSpot虚拟机的C1、C2编译器,Graal编译器。
  • 提前编译器:JDK的Jaotc、GNU Compiler for the Java(GCJ)[2]、Excelsior JET

这3类过程中最符合普通程序员对Java程序编译认知的应该是第一类,本文中的“前端”指的也是这种由前端编译器完成的编译行为。限制了“编译期”的范围后,我们对于“优化”二字的定义也需要放宽一些,因为Javac这类前端编译器对代码的运行效率几乎没有任何优化措施可言(在JDK 1.3之后,Javac的-O优化参数就不再有意义),哪怕是编译器真的采取
了优化措施也不会产生什么实质的效果。因为Java虚拟机设计团队选择把对性能的优化全部集中到运行期的即时编译器中,这样可以让那些不是由Javac产生的Class文件(如JRuby、Groovy等语言的Class文件)也同样能享受到编译器优化措施所带来的性能红利。但是,如果把“优化”的定义放宽,把对开发阶段的优化也计算进来的话,Javac确实是做了许多针对Java语言编码过程的优化措施来降低程序员的编码复杂度、提高编码效率。相当多新生的Java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖字节码或者Java虚拟机的底层改进来支持。我们可以这样认为,Java中即时编译器在运行期的优化过程,支撑了程序执行效率的不断提升;而前端编译器在编译期的优化过程,则是支撑着程序员的编码效率和语言使用者的幸福感的提高。

一. Javac编译过程

  1. 准备过程:初始化插入式注解处理器

  2. 解析与填充符号表过程。包括:

    • 词法解析:将源码中的字符流转换为标记集合的过程

      词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量和运算符都可以成为标记,如 int a = b +2这句代码包含了6个标记,分别是int、a、=、b、+、2。

    • 语法解析:根据标记序列构造出抽象语法树的过程,它的每个节点都代表一个语法结构
      语法分析是根据Token序列来构造抽象语法树的过程,抽象语法树(AST,Abstract Syntax Tree)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、 修饰符、运算符、接口、返回值甚至连代码注释等都可以是一个语法结构

    • 充符号表:符号表是由一组符号地址和符号信息构成的数据结构,它所登记的内容用于语义检查和产生中间代码,在目标代码生成阶段,对符号进行地址分配时,符号表是地址分配的直接依据

  3. 插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段

    • 允许读取、修改、添加抽象语法树中的任意元素。如果在处理注解期间对语法树进行过修改,编译器将回到解析和填充符号表的过程重新处理。
    • 著名的Lombok就是通过注解来实现自动生产getter/setter方法、进行空置检查、生成受查异常表、产生equals()和hashCode()等
  4. 分析与字节码生成过程,包括:

    • 标注检查。对语法的静态信息进行检查。包括变量使用前是否被声明、变量与赋值之间的数据类型是否能够匹配等。它还有一个常量折叠的一个优化,如a=1+2会优化成a=3。
    • 数据流及控制流分析。它是对程序上下问逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否赋值、方法的每条路径是否有返回值、是否所有的受查异常读被正确处理等。如方法中的 final属性就是在该阶段进行检查,因为局部变量的标志不会被Class文件保存,加载的验证阶段无法进行检查。
    • 解语法糖。将简化代码编写的语法糖还原成原有的形式。如泛型、自动拆箱装箱、变长参数等
      字节码生成。将前面各个步骤所生成的信息转化成字节码。编译器还进行了少量的代码添加和转换工作,如实例构造器和类构造器方法都是在该阶段添加的。编译器会把变量初始化、语句块等
    • 操作收敛到构造方法中(方法调用父类的实例构造器,则不用调用父类的类构造器,因为虚拟机会保证其父类会加载进入)

注意:第三阶段可能会产生新的符号,如果有新的符号产生,就必须转回第二阶段重新处理新的符号

二. Java语法糖的味道

1. 泛型

泛型是JDK1.5新增特性,本质是参数化类型(Parametersized Type)的应用,即数据类型被指定为参数
这种参数类型可用在类、接口和方法的创建中,分别称泛型类、泛型接口和泛型方法

泛型技术在C#和Java中的根本性分歧

C#泛型:

无论在程序源码、编译后的IL(Intermediate Language,中间语言,这时泛型是一个占位符)、运行期的CLR都切实存在
List与List是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据
这种实现称类型膨胀
基于这种方法实现的泛型称真实泛型

Java泛型:

只在程序源码中存在,在编译后的字节码文件中替换为原生类型(Raw Type,也称裸类型),且在相应位置插入强制转型代码
对于运行期Java语言ArrayList与ArrayList是同一个类
Java泛型技术实际上是Java的一颗语法糖
Java泛型实现方法称类型擦除
注,所谓的擦除仅是对方法Code属性中的字节码进行擦除,实际上元数据中保留了泛型信息,这是能通过反射手段取得参数化类型的根本依据
基于这种方法实现的泛型称伪泛型

当泛型遇见重载

  1. 入参不同出参相同
    public static void method(List list){}
    public static void method(List list){}
    以上代码不能被编译
    因编译后参数List和List被擦除,变成相同的原生类型List,导致两个方法的特征签名和描述符完全相同,不可共存于一个Class文件
  2. 入参不同出参不同
    public static String method(List list){}
    public static Int method(List list){}

以上代码可以被编译和执行
因两个方法的返回值不同,即,虽特征签名相同但描述符不同,可共存于一个Class文件
上述方法特征签名指Java语言中的特征签名,内容包括方法名称、参数顺序、参数类型
此外还有字节码中的特征签名,内容包括方法名称、参数顺序、参数类型、方法返回值、受查异常表

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

/**
 * 自动装箱、拆箱与遍历循环
 */
public static void main(String... args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4);
    // 若JDK1.7可写成 List<Integer> list =[1, 2, 3, 4];
    int sum = 0;
    for (int i : list) {
        sum += i;
    }
    System.out.println(sum);
}
/**
 * 自动装箱、拆箱与遍历循环编译后
 */
public static void main(String[] args) {
    List list = Arrays.asList(new Integer[]{
            Integer.valueOf(1), 
            Integer.valueOf(2), 
            Integer.valueOf(3), 
            Integer.valueOf(4)
    });
    int sum = 0;
    for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
        int i = ((Integer) localIterator.next()).intValue();
        sum += i;
    }
    System.out.println(sum);
}

包装类“==”运算不遇到算术运算时不会自动拆箱,equals()方法不处理数据转型关系

3. 条件编译

Java语言条件编译的实现:
根据布尔常量值真假,编译器把分支中不成立的代码块消除掉
这一工作在编译器解除语法糖阶段(com.sun.tools.javac.comp.Lower类)完成

/**
 * 条件编译
 */
public static void main(String[] args) {
    if (true) {
        System.out.println(" block 1");
    } else {
        System.out.println(" block 2");
    }
}

/**
 * 条件编译反编译
 */
public static void main(String[] args) {
    System.out.println(" block 1");
}

3. 自定义注解处理器

一套编程语言中编译子系统的优劣,很大程度上决定了程序运行性能的好坏和编码效率的高低,尤其在Java语言中,运行期即时编译与虚拟机执行子系统非常紧密地互相依赖、配合运作。了解JDK如何编译和优化代码,有助于我们写出适合JDK自优化的程序。看过javac源码,我们就知道,当我们的编译器在把java文件编译为字节码的时候,会对java源程序做各方面的校验,在本文的实战中,我们将会使用注解处理器API来编写一款拥有自己编码风格的校验工具,为Javac编译器添加一个额外的功能,在编译程序时检查程序名是否符合上述对类(或接口)、方法、字段的命名要求。

代码实现

我们实现注解处理器的代码需要继承抽象类javax.annotation.processing.AbstractProcessor,这个抽象类中只有一个必须覆盖的abstract方法:“process()”,它是Javac编译器在执行注解处理器代码时要调用的过程,我们可以从这个方法的第一个参数“annotations”中获取到此注解处理器所要处理的注解集合,从第二个参数“roundEnv”中访问到当前这个Round中的语法树节点,每个语法树节点在这里表示为一个Element。

注解处理器除了process()方法及其参数之外,还有两个可以配合使用的Annotations:@SupportedAnnotationTypes和@SupportedSourceVersion,前者代表了这个注解处理器对哪些注解感兴趣,可以使用星号“*”作为通配符代表对所有的注解都感兴趣,后者指出这个注解处理器可以处理哪些版本的Java代码。

每一个注解处理器在运行的时候都是单例的,如果不需要改变或生成语法树的内容,process()方法就可以返回一个值为false的布尔值,通知编译器这个Round中的代码未发生变化,无须构造新的JavaCompiler实例,在这次实战的注解处理器中只对程序命名进行检查,不需要改变语法树的内容,因此process()方法的返回值都是false。

注解处理器NameCheckProcessor.java:

package cn.tf.jvm.part10;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;

// 可以用"*"表示支持所有Annotations
@SupportedAnnotationTypes("*")
// 只支持JDK 1.8的Java代码
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class NameCheckProcessor extends AbstractProcessor {

private NameChecker nameChecker;

/**
 * 初始化名称检查插件
 */
@Override
public void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    nameChecker = new NameChecker(processingEnv);
}

/**
 * 对输入的语法树的各个节点进行进行名称检查
 */
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (!roundEnv.processingOver()) {
	    for (Element element : roundEnv.getRootElements())
	    	nameChecker.checkNames(element);
    	}
    	return false;
    }
}

从上面代码可以看出,NameCheckProcessor能处理基于JDK 1.8的源码,它不限于特定的注解,对任何代码都“感兴趣”,而在process()方法中是把当前Round中的每一个RootElement传递到一个名为NameChecker的检查器中执行名称检查逻辑。

然后来看NameChecker.java,它通过一个继承于javax.lang.model.util.ElementScanner6的NameCheckScanner类,以Visitor模式来完成对语法树的遍历,分别执行visitType()、visitVariable()和visitExecutable()方法来访问类、字段和方法,这3个visit方法对各自的命名规则做相应的检查,checkCamelCase()与checkAllCaps()方法则用于实现驼式命名法和全大写命名规则的检查。

package cn.tf.jvm.part10;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementScanner6;
import javax.lang.model.util.ElementScanner8;

import java.util.EnumSet;

import static javax.lang.model.element.ElementKind.*;
import static javax.lang.model.element.Modifier.*;
import static javax.tools.Diagnostic.Kind.WARNING;

/**
 * 程序名称规范的编译器插件:<br>
 * 如果程序命名不合规范,将会输出一个编译器的WARNING信息
 */
public class NameChecker {
private final Messager messager;

NameCheckScanner nameCheckScanner = new NameCheckScanner();

NameChecker(ProcessingEnvironment processsingEnv) {
this.messager = processsingEnv.getMessager();
}

/**
 * 对Java程序命名进行检查,根据《Java语言规范》第三版第6.8节的要求,Java程序命名应当符合下列格式:
 *
 * <ul>
 * <li>类或接口:符合驼式命名法,首字母大写。
 * <li>方法:符合驼式命名法,首字母小写。
 * <li>字段:
 * <ul>
 * <li>类、实例变量: 符合驼式命名法,首字母小写。
 * <li>常量: 要求全部大写。
 * </ul>
 * </ul>
 */
public void checkNames(Element element) {
     nameCheckScanner.scan(element);
}

/**
 * 名称检查器实现类,继承了JDK 1.6中新提供的ElementScanner6<br>
 * 将会以Visitor模式访问抽象语法树中的元素
 */
private class NameCheckScanner extends ElementScanner8<Void, Void> {

/**
 * 此方法用于检查Java类
 */
@Override
public Void visitType(TypeElement e, Void p) {
    scan(e.getTypeParameters(), p);
    checkCamelCase(e, true);
    super.visitType(e, p);
    return null;
}

/**
 * 检查方法命名是否合法
 */
@Override
public Void visitExecutable(ExecutableElement e, Void p) {
    if (e.getKind() == METHOD) {
    	Name name = e.getSimpleName();
    if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
    	messager.printMessage(WARNING, "一个普通方法 “" + name + "”不应当与类名重复,避免与构造函数产生混淆", e);
    	checkCamelCase(e, false);
    }
    super.visitExecutable(e, p);
    return null;
}

/**
 * 检查变量命名是否合法
 */
@Override
public Void visitVariable(VariableElement e, Void p) {
    // 如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查
    if (e.getKind() == ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
    	checkAllCaps(e);
    else
    	checkCamelCase(e, false);
    return null;
}

/**
 * 判断一个变量是否是常量
 */
private boolean heuristicallyConstant(VariableElement e) {
    if (e.getEnclosingElement().getKind() == INTERFACE)
    	return true;
    else if (e.getKind() == FIELD && e.getModifiers().containsAll(EnumSet.of(PUBLIC, STATIC, FINAL)))
    	return true;
    else {
    	return false;
    }
}

/**
 * 检查传入的Element是否符合驼式命名法,如果不符合,则输出警告信息
 */
private void checkCamelCase(Element e, boolean initialCaps) {
    String name = e.getSimpleName().toString();
    boolean previousUpper = false;
    boolean conventional = true;
    int firstCodePoint = name.codePointAt(0);
    
    if (Character.isUpperCase(firstCodePoint)) {
    	previousUpper = true;
	    if (!initialCaps) {
	    	messager.printMessage(WARNING, "名称“" + name + "”应当以小写字母开头", e);
	    	return;
	    }
    } else if (Character.isLowerCase(firstCodePoint)) {
	    if (initialCaps) {
	    	messager.printMessage(WARNING, "名称“" + name + "”应当以大写字母开头", e);
	    	return;
	    }
    } else
    	conventional = false;
    
    if (conventional) {
    int cp = firstCodePoint;
    for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
    cp = name.codePointAt(i);
    if (Character.isUpperCase(cp)) {
	    if (previousUpper) {
	    	conventional = false;
	    	break;
	    }
	    previousUpper = true;
    } else
    	previousUpper = false;
    }
}

if (!conventional)
	messager.printMessage(WARNING, "名称“" + name + "”应当符合驼式命名法(Camel Case Names)", e);
}

/**
 * 大写命名检查,要求第一个字母必须是大写的英文字母,其余部分可以是下划线或大写字母
 */
private void checkAllCaps(Element e) {
    String name = e.getSimpleName().toString();
    
    boolean conventional = true;
    int firstCodePoint = name.codePointAt(0);
    
    if (!Character.isUpperCase(firstCodePoint))
   		conventional = false;
    else {
    boolean previousUnderscore = false;
    int cp = firstCodePoint;
    for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
    cp = name.codePointAt(i);
    if (cp == (int) '_') {
	    if (previousUnderscore) {
	    	conventional = false;
	    	break;
	    }
    	previousUnderscore = true;
    } else {
    	previousUnderscore = false;
    if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
    	conventional = false;
    	break;
    }
    }
  }
}

if (!conventional)
	messager.printMessage(WARNING, "常量“" + name + "”应当全部以大写字母或下划线命名,并且以字母开头", e);
}
}
}

在BADLY_NAMED_CODE.java中包含多个不规范的命名,我们需要用到前面的两个类来校验下面这个文件是否符合要求。


	package cn.tf.jvm.part10;

	public class BADLY_NAMED_CODE {
    	enum colors {
        red, blue, green;
    }

    static final int _FORTY_TWO = 66;

    public static int NOT_A_CONSTANT = _FORTY_TWO;

    protected void BADLY_NAMED_CODE() {
        return;
    }

    public void NOTcamelCASEmethodNAME() {
        return;
    }
	}


运行和测试

我们可以通过Javac命令的“-processor”参数来执行编译时需要附带的注解处理器,在相应的工程下src/java/mian目录下执行以下命令编译

javac -encoding UTF-8 cn/tf/jvm/part10/NameChecker.java
javac -encoding UTF-8 cn/tf/jvm/part10/NameCheckProcessor.java

最后使用编译好的文件进行使用
javac -processor cn.tf.jvm.part10.NameCheckProcessor cn/tf/jvm/part10/BADLY_NAMED_CODE.java

执行结果如下:

cn\tf\jvm\part10\BADLY_NAMED_CODE.java:3: 警告: 名称“BADLY_NAMED_CODE”应当符合驼式命名法(Camel Case Names)
public class BADLY_NAMED_CODE {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:4: 警告: 名称“colors”应当以大写字母开头
enum colors {
^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“red”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“blue”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
 ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“green”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:8: 警告: 常量“_FORTY_TWO”应当全部以大写字母或下划线命名,并且以字母开头
static final int _FORTY_TWO = 66;
 ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:10: 警告: 名称“NOT_A_CONSTANT”应当以小写字母开头
public static int NOT_A_CONSTANT = _FORTY_TWO;
  ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:12: 警告: 一个普通方法 “BADLY_NAMED_CODE”不应当与类名重复,避免与构造函数产生混淆
protected void BADLY_NAMED_CODE() {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:12: 警告: 名称“BADLY_NAMED_CODE”应当以小写字母开头
protected void BADLY_NAMED_CODE() {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:16: 警告: 名称“NOTcamelCASEmethodNAME”应当以小写字母开头
public void NOTcamelCASEmethodNAME() {
^
10 个警告

扩展

Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,javac编译入口,重要类源码如下:

public void compile(List<JavaFileObject> sourceFileObjects,
                    List<String> classnames,
                    Iterable<? extends Processor> processors)
    throws IOException // TODO: temp, from JavacProcessingEnvironment
{
    if (processors != null && processors.iterator().hasNext())
        explicitAnnotationProcessingRequested = true;
    // as a JavaCompiler can only be used once, throw an exception if
    // it has been used before.
    if (hasBeenUsed)
        throw new AssertionError("attempt to reuse JavaCompiler");
    hasBeenUsed = true;

    start_msec = now();
    try {
    	/**
    	 * 插入注解处理
    	 */
        initProcessAnnotations(processors);

        /**
         * 词法分析、语法分析
         * parseFiles(sourceFileObjects) 分析源码。获取语法树JCCompilationUnit 集合
         * 
         * 填充符号表
         * enterTrees() 抽象语法树的顶局节点都先被放到待处理列表中并逐个处理列表中的节点。
         * 所有的类符号被输入到外围作用域的符号表中确定类的参数(对泛型类型而言)、超类型和接口
         * 如果需要添加默认构造器,将类中出现的符号输入到类自身的符号表中。
         */
        // These method calls must be chained to avoid memory leaks
        delegateCompiler =
            processAnnotations(
                enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                classnames);
        
        /**
         * 语义分析
         */
        delegateCompiler.compile2();
        delegateCompiler.close();
        elapsed_msec = delegateCompiler.elapsed_msec;
    } catch (Abort ex) {
        if (devVerbose)
            ex.printStackTrace();
    } finally {
        if (procEnvImpl != null)
            procEnvImpl.close();
    }
}

总结

NameCheckProcessor的实战例子只演示了JSR-269嵌入式注解处理器API中的一部分功能,基于这组API支持的项目还有用于校验Hibernate标签使用正确性的本质上与NameCheckProcessor所做的事情差不多)、自动为字段生成getter和setter方法的Lombok等

猜你喜欢

转载自blog.csdn.net/haiyanghan/article/details/109039626