• Java高级——前端编译和优化


    概述

    前端编译器把java文件编译成class文件,如:JDK的Javac、Eclipse JDT中的增量式编译器(ECJ)

    • 前端编译器对代码的运行效率无任何优化措施
    • 性能优化集中到运行期的即时编译器中,这样可适用于非Javac生成的Class文件
    • 但有针对编码过程的优化,从而降低编码复杂度,如语法糖

    Java虚拟机规范》未规定如何将java文件编译为Class文件,给予了较大的灵活性

    编译过程与具体编译器实现绑定,可能会导致某些代码在Javac中可以编译,而在ECJ则不可编译

    Javac导入

    Javac编译器由纯Java实现

    • 源码放在/langtools/src/share/classes/com/sun/tools/javac
    • JDK9后放在/src/jdk.compiler/share/classes/com/sun/tools/javac

    下面导入JDK8中的Javac源码,新建项目,将源码langtools/src/share/classes中的com/sun复制到项目目录(根据提示将private改为package即可解决报错)

    在这里插入图片描述

    将Run-RunConfiguration-Main中的Mian Class指定为javac.Main

    在这里插入图片描述

    新建一个Test.java,在Argument-Program argument指定其路径

    在这里插入图片描述

    点击Run,即可编译出Test.class,与在命令行中运行javac Test.java效果是一样的

    在这里插入图片描述

    Javac编译过程

    • 初始化插入式注解处理器
    • 解析与填充符号表
    • 注解处理
    • 分析与字节码生成

    在这里插入图片描述

    Javac编译动作的入口是
    com.sun.tools.javac.main.JavaCompiler类,关键代码如下

    在这里插入图片描述

    解析与填充符号表

    解析

    解析由parseFiles()方法完成,包括词法分析和语法分析

    词法分析将源代码的字符流转变为标记(Token)集合

    • 字符是程序最小元素,但标记才是编译时的最小元素
    • 关键字、变量名、字面量、运算符都可以作为标记
    • 由com.sun.tools.javac.parser.Scanner类实现
    • 如int a=b+2包含了6个标记,分别是int、a、=、b、+、2

    语法分析根据标记序列构造抽象语法树

    • 抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式
    • 其节点都代表着程序代码中的一个语法结构(Syntax Construct),如包、类型等
    • 由com.sun.tools.javac.parser.Parser类构造,com.sun.tools.javac.tree.JCTree类表示

    如下是Eclipse AST View插件分析出来的某段代码的抽象语法树视图

    在这里插入图片描述
    生成语法树以后,编译器不会再对源码字符流进行操作了,后续操作都建立在抽象语法树之上

    填充符号表

    填充符号表由enterTrees()方法完成,符号表(Symbol Table)是由一组符号地址和符号信息构成的数据结构

    • 在语义分析阶段用于语义检查(如检查一个名字的使用和原先的声明是否一致)和产生中间代码
    • 在目标代码生成阶段根据符号表对符号名进行地址分配
    • 由com.sun.tools.javac.comp.Enter类实现
    • 生成一个待处理列表,包含了每一个编译单元的抽象语法树和package-info.java的顶级节点

    注解处理过程

    注解处理由processAnnotations()方法完成,JDK6之前注解只会在程序运行时发挥作用,JDK6设计了插入式注解处理器API将注解处理提前至编译期

    可把插入式注解处理器看作编译器的插件,允许读取、修改、添加抽象语法树中的任意元素,如注解来实现自动产生getter/setter方法、生成受查异常表等

    若在处理注解期间修改过语法树,编译器将回到解析及填充符号表的过程重新处理,直至不再修改语法树,循环过程称为一个轮次(Round)

    若有新的注解处理器,会通过com.sun.tools.javac.processing.JavacProcessing-Environment类的doProcessing()方法生成一个新的JavaCompiler对象

    分析和字节码生成

    分析和字节码生成由Compile2()方法完成,包括标注检查、数据流及控制流分析、解语法糖、字节码生成

    在这里插入图片描述
    分析指语义分析,对结构上正确的源程序进行上下文相关性质的检查

    抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的,如下

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

    可能出现

    int d = a + c;
    int d = b + c;
    char d = a + c;
    
    • 1
    • 2
    • 3

    上面3种赋值都能构成正确的抽象语法树,但只有第一种赋值在Java语义上是正确的(IDE的红线错误提示即来源于语义分析)

    标注检查

    标注检查由attribute()完成,实现类是com.sun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check

    标注检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配

    在标注检查中,会进行一个称为常量折叠(Constant Folding)的代码优化,如对于

    int a = 1 + 2;
    
    • 1

    在抽象语法树上仍然能看到字面量“1”“2”和操作符“+”号,但是在经过常量折叠优化之后,会变成字面量“3”,

    数据流及控制流分析

    数据流及控制流分析由flow()方法完成,实现类为com.sun.tools.javac.comp.Flow

    对程序上下文逻辑更进一步的验证,如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值等

    编译时和类加载时的数据及控制流分析基本一样,但校验范围有所区别,有一些校验项只能在编译期或运行期进行

    public void foo(final int arg) {
    	final int var = 0;
    }
    public void foo(int arg) {
    	int var = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如上,一个方法有final,一个没有,但它们编译出来的指令都是一模一样

    • 局部变量在常量池中并没有CONSTANT_Fieldref_info
    • 也无存储有访问标志(access_flags)的信息
    • 故Class文件不会知道局部变量是否为final
    • final的不可变性仅由Javac在编译期间保证

    解语法糖

    解语法糖由desugar()方法完成,实现类为com.sun.tools.javac.comp.TransTypes和com.sun.tools.javac.comp.Lower

    语法糖指在计算机语言中添加的某种语法,其对语言的编译结果和功能并没有实际影响,但是却能更方便使用该语言

    Java中的语法糖如泛型、变长参数、自动装箱拆箱等,JVM运行时并不直接支持这些语法,需在编译阶段将其还原回原始的基础语法结构,称为解语法糖

    字节码生成

    字节码生成由generate()方法完成,实现类为com.sun.tools.javac.jvm.Gen

    把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,还进行了代码添加()和转换(将+转为StringBuffer的append)

    完成对语法树的遍历和调整之后,就会把的符号表交到com.sun.tools.javac.jvm.ClassWriter的writeClass()方法输出字节码,生成Class文件

    前端编译优化

    泛型

    泛型将操作的数据类型指定为方法签名中的一种特殊参数,用于类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法

    Java和C#的泛型

    C#是具现化式泛型(Reified Generics)

    • 在源码、编译后的中间语言或是运行期都是实际存在的
    • 运行期的List与List是两个不同的类型,有独立的虚方法表和类型数据

    Java是类型擦除式泛型(Type Erasure Generics)

    • 只在程序源码中存在,在Class中泛型被替换为原来的裸类型(Raw Type),并且加入强制转型代码
    • 运行期的ArrayList与ArrayList是同一个类型

    Java泛型使用效果(无法instanceof、创建对象和数组)和运行效率(需不断拆箱装箱)都不如C#,唯一优势在于实现只需要修改Javac,不需要改动字节码和JVM,也保证了以前没有使用泛型的库也可运行在JDK5之上

    泛型擦除

    为了保证兼容性,定义裸类型为所有该类型泛型化实例的共同父类型(Super Type),如下

    ArrayList ilist = new ArrayList();
    ArrayList slist = new ArrayList();
    ArrayList list; // 裸类型
    list = ilist;
    list = slist;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在编译时把ArrayList还原回ArrayList,在元素访问、修改时自动插入一些强制类型转换和检查指令

    public static void main(String[] args) {
    	Map map = new HashMap();
    	map.put("hello", "你好");
    	map.put("how are you?", "吃了没?");
    	System.out.println(map.get("hello"));
    	System.out.println(map.get("how are you?"));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对于如上代码,编译成Class,再反编译,程序变回泛型出现之前的写法,泛型都变回了裸类型,在元素访问时插入强制转型代码

    public static void main(String[] args) {
    	Map map = new HashMap();
    	map.put("hello", "你好");
    	map.put("how are you?", "吃了没?");
    	System.out.println((String) map.get("hello"));
    	System.out.println((String) map.get("how are you?"));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    泛型缺点

    泛型擦除无法支持原始类型,如下

    ArrayList list = new ArrayList();
    
    • 1

    导致只能使用包装类型,运行速度慢

    ArrayList list = new ArrayList();
    
    • 1

    运行时无法获取泛型类型信息

    if (item instanceof E) { // 不合法,无法对泛型进行实例判断
    
    }
    E newItem = new E(); // 不合法,无法使用泛型创建对象
    E[] itemArray = new E[10]; // 不合法,无法使用泛型创建数组
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如写一个泛型版本的从List到数组的转换方法,需显式传递数组的组件类型componentType

    public static  T[] convert(List list, Class componentType) {
    	T[] array = (T[])Array.newInstance(componentType, list.size());
    }
    
    • 1
    • 2
    • 3

    为了在反射中获取参数化类型,新增Signature属性用于存储一个方法在字节码层面的特征签名,其包含了参数化类型的信息

    其他

    如下,包含泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖

    public static void main(String[] args) {
    	List list = Arrays.asList(1, 2, 3, 4);
    	int sum = 0;
    	for (int i : list) {
    		sum += i;
    	}
    	System.out.println(sum);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    反编译后

    • 自动装箱、拆箱转化为对应的包装和还原方法
    • 遍历循环把代码还原成了迭代器实现
    • 变长参数变成了一个数组类型的参数
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    条件编译

    使用条件为常量的if语句,会触发条件编译,如下

    public static void main(String[] args) {
    	if (true) {
    		System.out.println("block 1");
    	} else {
    		System.out.println("block 2");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在编译时,就会优化

    public static void main(String[] args) {
    	System.out.println("block 1");
    }
    
    • 1
    • 2
    • 3

    若非if语句,则可能在控制流分析中提示错误且拒绝编译,如下

    public static void main(String[] args) {
    	// 编译器将会提示“Unreachable code”
    	while (false) {
    		System.out.println("");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实战:插入式注解处理器

    在前端编译这部分,可通过插入式注解处理器API影响Java编译子系统的行为

    接下来基于插入式注解处理器API编写一个校验工具NameCheckProcessor规范书写格式:

    • 类(或接口):驼式命名法,首字母大写
    • 方法:驼式命名法,首字母小写
    • 类或实例变量:符合驼式命名法,首字母小写。
    • 常量:由大写字母或下划线构成,首字符不能是下划线

    代码实现

    继承抽象类javax.annotation.processing.AbstractProcessor,实现抽象方法process(),其是Javac在执行注解处理器代码时的回调,如下,把当前轮次中的每一个RootElement传递到NameChecker执行名称检查逻辑

    import java.util.Set;
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.RoundEnvironment;
    import javax.annotation.processing.SupportedAnnotationTypes;
    import javax.annotation.processing.SupportedSourceVersion;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    
    @SupportedAnnotationTypes("*") //设置要处理的注解
    @SupportedSourceVersion(SourceVersion.RELEASE_6) //设置处理的Java版本
    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 annotations, RoundEnvironment roundEnv) {
            if (!roundEnv.processingOver()) {
                for (Element element : roundEnv.getRootElements())
                    nameChecker.checkNames(element);
            }
            return false;	//不需要改变或添加抽象语法树,返回false,编译器不构造新的JavaCompiler实例
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 域processingEnv是父类中的一个protected变量,用于创建新的代码、向编译器输出信息、获取其他工具类等
    • visitType()、visitVariable()和visitExecutable()访问类、字段和方法
    • checkCamelCase()与checkAllCaps()检查驼式命名法和全大写命名
    import java.util.EnumSet;
    import javax.annotation.processing.Messager;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.ElementKind;
    import javax.lang.model.element.ExecutableElement;
    import javax.lang.model.element.Modifier;
    import javax.lang.model.element.Name;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.element.VariableElement;
    import javax.lang.model.util.ElementScanner6;
    import javax.tools.Diagnostic;
    
    
    public class NameChecker {
        private final Messager messager;
        NameCheckScanner nameCheckScanner = new NameCheckScanner();
    
        NameChecker(ProcessingEnvironment processsingEnv) {
            this.messager = processsingEnv.getMessager();
        }
    
        public void checkNames(Element element) {
            nameCheckScanner.scan(element);
        }
    
        /**
         * 名称检查器实现类,继承了JDK 6中的ElementScanner6
         * 将会以Visitor模式访问抽象语法树中的元素
         */
        private class NameCheckScanner extends ElementScanner6 {
            /**
             * 此方法用于检查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(Diagnostic.Kind.WARNING, "一个普通方法 “" + name + "”不应当与类名重复,避免与构造函数产生混淆", e);
                    	checkCamelCase(e, false);
                }
                super.visitExecutable(e, p);
                return null;
            }
    
            /**
             * 检查变量命名是否合法
             */
            @Override
            public Void visitVariable(VariableElement e, Void p) {
                // 如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查
                if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
                    checkAllCaps(e);
                else
                    checkCamelCase(e, false);
                return null;
            }
    
            /**
             * 判断一个变量是否是常量
             */
            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是否符合驼式命名法,如果不符合,则输出警告信息
             */
            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(Diagnostic.Kind.WARNING, "名称“" + name + "”应当以小写字母开头", e);
                        return;
                    }
                } else if (Character.isLowerCase(firstCodePoint)) {
                    if (initialCaps) {
                        messager.printMessage(Diagnostic.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(Diagnostic.Kind.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(Diagnostic.Kind.WARNING, "常量“" + name + "”应当全部以大写字母或下划线命名,并且以字母开头", e);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156

    验证

    使用如下代码验证

    public class BADLY_NAMED_CODE {
        enum colors {
            red, blue, green;
        }
    
        static final int _FORTY_TWO = 42;
        public static int NOT_A_CONSTANT = _FORTY_TWO;
    
        protected void BADLY_NAMED_CODE() {
            return;
        }
    
        public void NOTcamelCASEmethodNAME() {
            return;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    利用上面导入的Javac,参数-processor指定执行编译时需要附带的注解处理器(有多个用逗号分隔)

    javac src/com/sun/NameChecker.java
    javac src/com/sun/NameCheckProcessor.java
    javac -processor com.sun.NameCheckProcessor src/com/sun/BADLY_NAMED_CODE.java
    
    • 1
    • 2
    • 3

    输出如下

    在这里插入图片描述

  • 相关阅读:
    代码优雅之道——Java如何判空
    Gerrit | 重磅! 2.x 版本升级到 3.x 版本----转
    docker install consul
    现代卷积神经网络 - 批量规范化
    Ubuntu18/20运行ORB-SLAM3
    Codeforces Round 515
    (4)全局路径切换、换道策略
    linux基础(2)
    【python】python内置函数——hasattr()判断对象是否包含某个属性,setattr()设置对象的属性值
    Java开发三年四面字节跳动斩获offer,掌握这些题进大厂没压力!
  • 原文地址:https://blog.csdn.net/qq_35258036/article/details/127808487