深入理解Java虚拟机_10_早期(编译期)优化

戏子无情 提交于 2020-03-10 17:04:54

10.1 概述

Java 语言的编译期是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实应该叫做“编译器的前端”)把 *.java 文件编译成 *.class 文件的过程;也可能是指虚拟机的后端运行期编译器(JIT 即时编译器,just in time compiler)把字节码转变为机器码的操作过程;还可能指使用静态提前编译器(AOT 编译器,Ahead of time compiler)把 *.java 文件编译成机器代码的过程。下面列举三个有代表性的编译器:

  • 前端编译器:Sun 的 javac、eclipse JDT 中的增量式编译器(ECJ)。
  • JIT 编译器:Hotspot VM 的 C1、C2 编译器。
  • AOT 编译器:GNU Compiler for the Java(GCJ)、Excelsior JET。

本章中的“编译期”和“编译器”都局限于第一类编译过程。Java 中即时编译器在运行期的优化过程对程序运行来说更重要,而前端编译器在编译期的优化对程序编码来说关系更密切。

10.2 Javac 编译器

我们开始分析下 javac 编译器的源码。

10.2.1 Javac 的源码与调试

我们可建立一个名为“Compiler_javac” 的 Java 工程,然后把 JDK_SRC_HOME/langtools/src/share/classes/com/sum/* 目录下的源文件全部复制到工程的源码目录下。

javac 编译器过程大致分为以下几个过程:

  • 解析与填充符号表过程
  • 插入式注解处理器的注解处理过程
  • 分析与字节码生成过程

Javac 编译动作的入口是 com.sun.tools.javac.main.JavaCompiler 类,上述三个过程的代码逻辑集中在这个类的 compiler() 和 compiler2() 方法中。下图展示了整个编译关键过程主要靠这 8 个方法来完成。

10.2.2 解析与填充符号表

解析步骤 有 parseFiles() 方法完成,包括了经典的程序编译原理中的词法分析和语法分析。

1. 词法、语法分析

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

语法分析是根据 Token 序列构造抽象语法树的过程,抽象语法树(AST)是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类、修饰符、运算符、接口、返回值甚至代码注释等都是一个语法结构。语法分析是由 com.sun.tools.javac.parser.Parser 类实现,这阶段生成的抽象语法树由 com.sun.tools.javac.tree.JCTree 来表示。经过这个步骤之后,编译器就基本不会对源码文件进行操作了,后续的操作都是建立在抽象语法树之上。

2. 填充符号表

下一步就是填充符号表的过程,即 enterTrees() 方法所做的事情。符号表(symbol table)是由一组符号地址和符号信息构成的表格。符号表中所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查和产生中间代码。在目标代码生成阶段,当对符号名地址分配时,符号表是地址分配的依据。符号填充由 com.sun.tools.javac.comp.Enter 来实现,此过程的出口是一个待处理列表(To Do list),包含了每一个编译单元的抽象语法树的顶级节点,以及 pack-info.java 的顶级节点。

10.2.3 注解处理器

JDK 1.5 出现了对注解的支持,JDK 1.6 提供了一组插入式注解处理器的标准 API 在编译期对注解进行处理,我们可以把它看做是一组编译期的插件,在这些插件里边,可以读取、修改、添加抽象语法树中任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译期将会回到解析以及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个 Round。

插入式注解处理器的初始化过程是 initProcessAnnotations() 方法中完成的,而它的执行过程则是在 processAnnotations() 方法中完成的,这个方法判断是否还有新的注解处理器需要执行,如果有的话,通过 com.sun.tools.javac.processing.JavacProcessingEnvironment 类的 doProcessing() 方法生成一个新的 JavaCompiler 对象编译的后续步骤进行处理。

10.2.4 语义分析与字节码生成

语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能表示结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。

而语义分析的主要任务就是对结构上正确的源程序进行上下文有关性质的审查,如类型审查,变量类型赋值计算转换等。

1. 标注检查

语义过程分为标注检查以及数据及控制流分析两个步骤,分别为 attribute() 和 flow() 两个方法完成。

标注检查的内容包括诸如变量使用前是否被声明、变量与赋值之间的数据类型是否能够匹配等。标注检查主要由 com.sun.tools.javac.comp.Attr 和 com.sun.tools.javac.comp.Check 类完成。

2. 数据以控制流分析

数据以控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否有返回值、是否所有的受检查异常都被正确处理了等问题。具体由 com.sun.tools.javac.comp.Flow 类实现。

3. 解语法糖

Java 中的语法糖主要是前面提到过的泛型、可变参数、自动装/拆箱等,虚拟机运行不支持这些语法,它们需要在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。它是由 com.sun.tools.javac.comp.TransTypes 和 com.sun.tools.javac.comp.Lower 类实现的。

4. 字节码生成

它是由 com.sun.tools.javac.jvm.Gen 类来完成。字节码生成阶段不仅仅把前面各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。

例如签名多次提到的 <init>()方法和 <clinit>() 方法就是这个阶段添加到语法树中的。

完成了对语法树的遍历和调整之后,就会把填充了所有需要信息的符号表交给 com.sun.tools.javac.jvm.ClassWriter 类,它将输出字节码,生成最终的 Class 文件。编译过程到此结束。

10.3 Java 语法糖的味道

10.3.1 泛型与类型擦除

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

Java 泛型在编译后字节码文件中,就已经被替换成原生类型(raw type)了,并在相应的地方插入了强转类代码。所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中泛型实现的方法称为泛型擦除,基于这种方式的实现的泛型称为伪泛型。

由于泛型的引入,JCP 组织对虚拟机规范做出了相应的修改,引入了诸如 Signature、LocalVariableTypeTable等新的属性用来解决伴随泛型而来的参数类型的识别问题,Signature 是一个重要的属性,它的作用是存储一个方法在字节码层面上的特征签名,这个属性保存的参数类型并不是原生类型,而是包括了参数化类型的信息。

Java 代码

package com.liukai.jvmaction.ch_10;

import java.util.List;

public class Test10 {

  public static void method() {
    System.out.println("m0");
  }

  public static void method(List<Integer> args) {
    System.out.println("m1");
  }

}

如下字节码:

  public static void method(java.util.List<java.lang.Integer>);
    descriptor: (Ljava/util/List;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String m1
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 16: 0
        line 17: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   Ljava/util/List;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   Ljava/util/List<Ljava/lang/Integer;>;
    Signature: #26                          // (Ljava/util/List<Ljava/lang/Integer;>;)V

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

自动装箱、拆箱在编译之后被转换成为对应的包装和还原方法,比如 Integer.value() 与 Integer.intValue() 方法,而遍历循环则把代码还原成了迭代器的实现。变长参数在调用时变成了一个数组类型的参数。

  // 自动装箱的陷阱
  public static void main(String[] args) {

    Integer a = 1;
    Integer b = 2;
    Integer c = 3;
    Integer d = 3;
    Integer e = 321;
    Integer f = 321;
    Long g = 3L;

    System.out.println(c == d);// true
    System.out.println(e == f);// false
    System.out.println(c == (a + b));// true
    System.out.println(c.equals(a + b));//true
    // 包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,以及它们的 equals() 方法不处理数据转型的关心
    System.out.println(g == (a + b));// true
    System.out.println(g.equals(a + b));// false
  }

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

10.3.3 条件编译

条件编译就是根据条件选择性的编译,在 Java 中进行条件编译方法是使用条件作为常量的 if 语句。

  public static void conditionCompiler() {
    if(true){
      System.out.println(1);
    }else{
      System.out.println(2);
    }
  }

编译之后的字节码文件

  public static void conditionCompiler();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         // 我们发现只有编译出 System.out.println(1); 的字节码
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
         7: return
      LineNumberTable:
        line 30: 0
        line 34: 7

通过上述代码我们发现字节码中只编译了System.out.println(1); 的字节码指令。

10.4 实战:插入式注解处理器

10.4.1 实战目标

通过 Javac 源码的分析,我们直到编译器把 Java 源码编译为字节码时,会对 Java 程序源码做个方面的检查校验。有鉴于此,业界出现了很多针对程序“写得好不好”的辅助校验工具,如 CheckStyle、FindBug、Klocwork等。这些代码校验工具有一些是基于 Java 的源码进行校验,还有一些是通过扫描字节码来完成。在本节中,我们将会使用注解处理器 API 来编写一款拥有自己编码风格的校验工具:NameCheckProcessor。

根据 Java 语言规范中要求,Java 程序的命令应当符合以下格式的书写规范:

  • 类(或接口):符合驼峰命名、首字母大写。
  • 方法:符合驼峰命令、首字母小写。
  • 字段:
    • 类或实例变量:符合驼峰命令,首字母小写。
    • 常量:要求全部由大写字母或下划线构成,第一个字母不能是下划线。

我们本次的实战目标就是为 Javac 编译器添加一个额外的功能,在编程程序时检查程序名是否符合上述对类、接口、方法、字段的命令要求。

10.4.2 代码实现

通过注解处理器的 API 实现一个编译器插件,要实现注解处理器的代码需要继承抽象类 javax.annotation.processing.AbstractProcessor。

实现它的一个 process() 抽象方法,它是 Javac 编译器在执行注解处理器代码时要调用的过程,第一个参数“annotations”可以获取到此注解处理器要处理的注解集合,第二个参数“roundEnv”可以访问到当前这个 Round 中的语法树节点,每个语法树节点在这里表示一个 Element。它包括 Java 中最常用的元素。

还要实现一个它初始化方法 init(),它的参数 processingEnv,代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例变量。

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

每一个注解处理器在运行时都是单例的,如果不需要改变或者生成语法树的内容,process() 方法可以返回一个 false 的布尔值,告诉编译器这个 Round 中的代码未发生变化,无须构造新的 JavaCompiler 实例。

以下给出

package com.liukai.jvmaction.ch_10;

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;

/**
 * 10-11 注解处理器 NameCheckProcessor
 */
//可以用"*"表示支持所有Annotations,可以使用通配符,如果需要不同包下的话可以使用{}进行分割
@SupportedAnnotationTypes("*")
//这里填写支持的java版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class NameCheckProcessor extends AbstractProcessor {

  private NameChecker nameChecker;

  /**
   * 初始化名称检查插件,processingEnv为注解处理器提供的上下文环境
   */
  @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;
  }

}

NameChecker 检查器的代码:

package com.liukai.jvmaction.ch_10;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementScanner8;
import javax.tools.Diagnostic.Kind;
import java.util.EnumSet;

/**
 * 10-12 命令检查器
 */
public class NameChecker {

  private final Messager messager;

  NameCheckScanner nameCheckScanner = new NameCheckScanner();

  public NameChecker(ProcessingEnvironment processingEnv) {

    this.messager = processingEnv.getMessager();
  }

  /**
   * 对java程序命名进行检查,java名称命名应符合如下格式:
   * <ul>
   * <li>类或接口:符合驼式命名法,首字母大写
   * <li>方法:符合驼式命名法,首字母小写
   * <li>字段:符合驼式命名法,首字母小写
   * <li>类、实例变量:符合驼式命名法,首字母小写
   * <li>常量:要求全部大写
   * </ul>
   */
  public void checkNames(Element element) {
    nameCheckScanner.scan(element);

  }

  /**
   * 名称检查器实现类,继承了jdk1.8中新提供的ElementScanner8<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() == ElementKind.METHOD) {
        Name name = e.getSimpleName();
        if (name.contentEquals(e.getEnclosingElement().getSimpleName())) {
          messager.printMessage(Kind.WARNING, " 一个普通方法 '" + name + "' 不应该与类名相同,避免与构造方法产生混淆", e);
        }
        checkCamelCase(e, false);
      }

      super.visitExecutable(e, p);
      return null;
    }

    /**
     * 检查变量命名是否合法
     */
    @Override
    public Void visitVariable(VariableElement e, Void p) {

      if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null
        || heuristicallyConstant(e)) {
        checkAllCaps(e);
      } else {
        checkCamelCase(e, false);
      }

      return null;
    }

    /**
     * 判断一个变量是否是常量
     *
     * @param e
     * @return
     */
    private boolean heuristicallyConstant(VariableElement e) {

      if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE) {
        return true;
      } else if (e.getKind() == ElementKind.FIELD && e.getModifiers()
        .containsAll(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL))) {
        return true;
      } else {
        return false;
      }
    }

    /**
     * 检查传入的Element是否符合驼式命名法,如果不符合则输出警告信息
     *
     * @param e
     * @param initialCaps
     */
    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(Kind.WARNING, "名称  '" + name + " ' 应当以小写字母开头", e);
          return;
        }
      } else if (Character.isLowerCase(firstCodePoint)) {
        if (initialCaps) {
          messager.printMessage(Kind.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(Kind.WARNING, "名称  '" + name + " ' 应当符合驼式命名法", e);
      }

    }

    /**
     * 大写命名检查,要求第一个字母是大写的英文字母,其余部分是大写字母或下划线
     *
     * @param e
     */
    private void checkAllCaps(VariableElement 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 == '_') {
            if (previousUnderscore) {
              conventional = false;
              break;
            }
            previousUnderscore = true;
          } else {
            previousUnderscore = false;
            if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
              conventional = false;
              break;
            }
          }
        }
      }
      if (!conventional) {
        messager.printMessage(Kind.WARNING, "常量  '" + name + " ' 应当全部以大写字母或下划线命名,并且以字母开头", e);
      }
    }

  }

}

这个类内部通过一个继承于 javax.lang.model.util.ElementScanner8 的 NameCheckScanner 类,以 Visitor 模式来完成对语法树的遍历,分别执行 visitType()、visitVariable()、visitExecutable()方法来访问类、字段和方法,对各自的命名规则做相应的检查。

package com.liukai.jvmaction.ch_10;

/**
 * 10-13 包含多个不规范命名的代码样例
 */
public class BADLY_NAME_CODE {

  static final int _FORTY_TWO = 42;

  public static int NOT_A_CONSTANT = _FORTY_TWO;

  protected void BADLY_NAMED_CODE() {
    return;
  }

  public void NOTCASEmethodNAME() {
    return;
  }

  enum colors {
    red, blue, green;
  }

}

10.4.3 运行与测试

我们通过 javac 命令的“-processor”参数来执行编译时需要附带的注解处理器,多个注解处理器用逗号隔开。还可以使用 -XprintRounds 和 -XprintProcessorInfo 参数来查看处理器运作的详细信息。

# 进入到项目的包根路径下
cd /Users/liukai/IdeaProjects/myproject/jvm-action/src/main/java

# 使用 javac 命令编译 NameChecker.java 文件
javac com/liukai/jvmaction/ch_10/NameChecker.java

# 使用 javac 命令编译 NameCheckProcessor.java 文件
javac com/liukai/jvmaction/ch_10/NameCheckProcessor.java

# 使用 javac 命令编译 BADLY_NAME_CODE.java 文件
javac -processor com.liukai.jvmaction.ch_10.NameCheckProcessor com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java

输出结果:
liukai•src/main/java(master⚡)» javac -processor com.liukai.jvmaction.ch_10.NameCheckProcessor com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java                                                         [15:14:12]
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:6: 警告: 名称  'BADLY_NAME_CODE ' 应当符合驼式命名法
public class BADLY_NAME_CODE {
       ^
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:20: 警告: 名称  'colors ' 应当以大写字母开头
  enum colors {
  ^
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:21: 警告: 常量  'red ' 应当全部以大写字母或下划线命名,并且以字母开头
    red, blue, green;
    ^
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:21: 警告: 常量  'blue ' 应当全部以大写字母或下划线命名,并且以字母开头
    red, blue, green;
         ^
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:21: 警告: 常量  'green ' 应当全部以大写字母或下划线命名,并且以字母开头
    red, blue, green;
               ^
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:8: 警告: 常量  '_FORTY_TWO ' 应当全部以大写字母或下划线命名,并且以字母开头
  static final int _FORTY_TWO = 42;
                   ^
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:10: 警告: 名称  'NOT_A_CONSTANT ' 应当以小写字母开头
  public static int NOT_A_CONSTANT = _FORTY_TWO;
                    ^
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:12: 警告: 名称  'BADLY_NAMED_CODE ' 应当以小写字母开头
  protected void BADLY_NAMED_CODE() {
                 ^
com/liukai/jvmaction/ch_10/BADLY_NAME_CODE.java:16: 警告: 名称  'NOTCASEmethodNAME ' 应当以小写字母开头
  public void NOTCASEmethodNAME() {
              ^
9 个警告

10.4.4 其他应用案例

NameCheckProcessor 的实战例子只展示了 JSR-269 嵌入式注解处理器 API 中的一部分功能,基于这组 API 支持的项目还有用于校验 Hibernate 标签使用正确性的 Hibernate Validator Annotation Processor、自动为字段生成 getter 和 setter 方法的 Project Lombok(根据已有元素生成新的语法树元素)等。

10.4.5 常用 Java 静态代码分析工具的分析与比较

我们上章中讲解的注解处理器是用在 javac 编译器中的,可以对源代码加载成的的语法树进行检查、分析、修改相关操作,接下里我们介绍的是静态代码扫描工具,它们不是基于注解处理器实现,但是所做的事情与我们上述编写的实战例子功能相似并且更加强大。

Checkstyle

checkstyle产生于2001年,是以antlr作为java语法分析器的静态源码分析工具。通过checkstyle的xml配置文件可指定源码分析规则。通过继承checkstyle自身的Check可实现新的代码检查逻辑。另外继承AbstractFileSetCheck可实现除java以外的其它编程语言的检查规则,不过checkstyle封装的antlr只能分析java语法,而且没有封装其它的语法分析器,因此如果要用checkstyle检查其它语言的代码需要封装或实现相应语言的语法分析工具。

ANTLR

代码检查工具选型

PMD

PMD产生于2002年,是以JAVACC作为java语法分析器的静态源码分析工具。PMD同样是首先把java源码解析成语法树(pmd用xml格式维护语法树),然后遍历语法树进行代码检查,只不过java语法分析器是javacc。

JavaCC 是一个词法分析生成器和语法分析生成器。词法分析和语法分析是处理输入字符序列的软件构件,编译器和解释器协同词法分析和语法分析来“解密”程序文件。它是一个单独项目,与 jdk 中的 javac 编译器没有直接关系。javac 编译器中有自己实现的词法分析、语法分析器。

JavaCC详解

findbugs

Findbugs产生于2003年,是基于bcel库通过扫描字节码完成代码检查的代码检查工具。只要是能编译成字节码的源文件都可用findbugs检查,但是需要对bcel库和字节码有相当了解。

阿里巴巴的代码规范插件是 Alibaba Java Coding Guidelines

阿里巴巴的代码规范插件是基于 PMD 规范开发的,PMD是一种代码静态分析工具,当使用PMD规则分析Java源码时,PMD首先利用JavaCC和EBNF文法产生了一个语法分析器,用来分析普通文本形式的Java代码,产生符合特定语法结构的语法,同时又在JavaCC的基础上添加了语义的概念即JJTree,通过JJTree的一次转换,这样就将Java代码转换成了一个AST,AST是Java符号流之上的语义层,PMD把AST处理成一个符号表。然后编写PMD规则,一个PMD规则可以看成是一个Visitor,通过遍历AST找出多个对象之间的一种特定模式,即代码所存在的问题。

阿里巴巴Java代码规约插件p3c-pmd使用指南与实现解析

常用 Java 静态代码分析工具的分析与比较

Java 静态分析工具 分析对象 应用技术
Checkstyle Java 源文件 缺陷模式匹配
FindBugs 字节码 缺陷模式匹配;数据流分析
PMD Java 源代码 缺陷模式匹配
Jtest Java 源代码 缺陷模式匹配;数据流分析

我们可以在开发工具 idea 中安装这些检查代码规范的插件,比如阿里巴巴的代码规范插件(Alibaba Java Coding Guidelines)、checkstyle 插件、sonarlint 等等插件。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!